diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index b06f3391d4..fc153dd632 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -79,6 +79,7 @@ Gershaft Giordani Gokce gordon +Griese grzhan Guo hanselman @@ -197,6 +198,7 @@ cortana dlnilsson fancymouse firefox +fudan gpt Inkscape Markdig @@ -212,6 +214,7 @@ regedit roslyn Spotify Vanara +wangyi WEX windowwalker winui @@ -219,7 +222,9 @@ winuiex wix wordpad WWL +wyhash xamlstyler Xavalon Xbox Youdao +zadjii diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index 912d72d472..21d7da66b8 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -96,10 +96,11 @@ ^\Qdoc/devdocs/localization.md\E$ ^\Qsrc/common/ManagedCommon/ColorFormatHelper.cs\E$ ^\Qsrc/common/notifications/BackgroundActivatorDLL/cpp.hint\E$ +^\Qsrc/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002.pdn\E$ ^\Qsrc/modules/colorPicker/ColorPickerUI/Assets/ColorPicker/colorPicker.cur\E$ ^\Qsrc/modules/colorPicker/ColorPickerUI/Shaders/GridShader.cso\E$ -^\Qsrc/modules/MouseUtils/MouseJumpUI/MainForm.resx\E$ ^\Qsrc/modules/MouseUtils/MouseJump.Common/NativeMethods/User32/UI/WindowsAndMessaging/User32.SYSTEM_METRICS_INDEX.cs\E$ +^\Qsrc/modules/MouseUtils/MouseJumpUI/MainForm.resx\E$ ^\Qsrc/modules/MouseWithoutBorders/App/Form/frmAbout.cs\E$ ^\Qsrc/modules/MouseWithoutBorders/App/Form/frmInputCallback.resx\E$ ^\Qsrc/modules/MouseWithoutBorders/App/Form/frmLogon.resx\E$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 6907f937c5..6e4439e4a2 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -13,6 +13,8 @@ AColumn acrt ACTIVATEAPP activationaction +ACVS +adaptivecards ADDSTRING ADDUNDORECORD ADifferent @@ -21,22 +23,21 @@ admx advfirewall AFeature AFFINETRANSFORM +affordances AFX AGGREGATABLE -ahk AHybrid akv -ALIGNRIGHT ALarger +ALIGNRIGHT ALLAPPS ALLCHILDREN ALLINPUT +Allman ALLOWUNDO ALLVIEW ALPHATYPE AModifier -AMPROPERTY -AMPROPSETID amr ANDSCANS animatedvisuals @@ -47,15 +48,19 @@ AOC aocfnapldcnfbofgmbbllojgocaelgdd APARTMENTTHREADED APeriod +apicontract apidl APIENTRY APIIs Apm APPBARDATA APPEXECLINK +APPICONREFERENCE APPLICATIONFRAMEHOST appmanifest +APPMODEL APPNAME +APPPUBLISHER appref appsettings appwindow @@ -124,6 +129,7 @@ BLURBEHIND BLURREGION bmi bms +BNDBk BNumber BODGY BOOTSTRAPPERINSTALLFOLDER @@ -144,6 +150,7 @@ bugreport BUILDARCH BUILDNUMBER buildtransitive +builttoroam BVal BValue byapp @@ -154,6 +161,7 @@ callbackptr calpwstr Cangjie CANRENAME +Cantarell CAPTUREBLT CAPTURECHANGED CARETBLINKING @@ -164,7 +172,7 @@ CCHFORMNAME CCom CContext CDeclaration -CDEF +cdn CElems CENTERALIGN certlm @@ -191,9 +199,12 @@ CLIPCHILDREN CLIPSIBLINGS closesocket CLSCTX +CLSIDs +Clsids Clusion cmder CMDNOTFOUNDMODULEINTERFACE +cmdpal CMIC CMINVOKECOMMANDINFO CMINVOKECOMMANDINFOEX @@ -205,16 +216,17 @@ coclass codereview Codespaces COINIT +colid colorconv colorformat colorhistory colorhistorylimit COLORKEY comctl -comdef comdlg comexp cominterop +commandpalette compmgmt COMPOSITIONFULL CONFIGW @@ -224,6 +236,7 @@ CONOUT contentfiles CONTEXTHELP CONTEXTMENUHANDLER +contractversion CONTROLL CONTROLPARENT copiedcolorrepresentation @@ -232,13 +245,17 @@ COREWINDOW cotaskmem COULDNOT countof +cpcontrols cph cplusplus CPower +cppwinrt createdump CREATEPROCESS CREATESCHEDULEDTASK CREATESTRUCT +CREATETHREAD +CREATEWINDOW CREATEWINDOWFAILED CRECT CRH @@ -249,8 +266,10 @@ CSettings cso CSRW CStyle +cswin CTest CTEXT +Ctl CTLCOLORSTATIC currentculture CURRENTDIR @@ -261,6 +280,7 @@ CUSTOMACTIONTEST CVal cvd CVirtual +CVS cxfksword CXSCREEN CXSMICON @@ -276,17 +296,24 @@ datareader datatracker dataversion Dayof +DBID DBLCLKS DBLEPSILON +DBPROP +DBPROPIDSET +DBPROPSET DCapture DCBA DCOM DComposition DCR ddd +DDEAPPLICATION +DDECOMMAND DDEIf +DDEIFEXEC +DDETOPIC DDevice -ddf DDxgi Deact debugbreak @@ -297,14 +324,18 @@ DEFAULTBOOTSTRAPPERINSTALLFOLDER DEFAULTCOLOR DEFAULTFLAGS DEFAULTICON +defaultlib DEFAULTONLY +DEFAULTTOFOLDER DEFAULTTONEAREST DEFAULTTONULL DEFAULTTOPRIMARY +DEFAULTTOSTAR DEFERERASE DEFPUSHBUTTON deinitialization DELA +DELEGATEEXECUTE DELETEDKEYIMAGE DELETESCANS deletethis @@ -319,15 +350,11 @@ DESKTOPABSOLUTEPARSING desktopshorcutinstalled devblogs devdocs -devenum devmgmt DEVMODE DEVMODEW -DEVMON -DEVSOURCE -DGR +devpal DIALOGEX -DIIRFLAG dimm DISABLEASACTIONKEY DISABLENOSCROLL @@ -336,8 +363,8 @@ DISPLAYCHANGE DISPLAYCONFIG DISPLAYFLAGS DISPLAYFREQUENCY -DISPLAYORIENTATION displayname +DISPLAYORIENTATION divyan Dlg DLGFRAME @@ -360,8 +387,9 @@ DRAWFRAME drawingcolor dreamsofameaningfullife drivedetectionwarning +Droid DROPFILES -dshow +DROPTARGET DSTINVERT DSurface DTexture @@ -370,11 +398,7 @@ Dutil DVASPECT DVASPECTINFO DVD -DVH -DVHD dvr -DVSD -DVSL DVTARGETDEVICE dwl dwm @@ -395,7 +419,6 @@ dwrite dxgi easeofaccess ecount -EData Edid EDITKEYBOARD EDITSHORTCUTS @@ -418,7 +441,6 @@ epu ERASEBKGND EREOF EResize -ERole ERRORIMAGE ERRORTITLE erwrite @@ -455,22 +477,22 @@ exsb exstyle EXTENDEDKEY EXTENDEDVERBS +extensionsdk EXTRALIGHT EXTRINSICPROPERTIES eyetracker FANCYZONESDRAWLAYOUTTEST FANCYZONESEDITOR FARPROC -fdw fff FILEEXPLORER FILEFLAGS FILEFLAGSMASK -FILEINFOSIG FILELOCKSMITH FILELOCKSMITHCONTEXTMENU FILELOCKSMITHEXT FILELOCKSMITHLIBINTEROP +filemgmt FILEMUSTEXIST FILEOP FILEOPENDIALOGOPTIONS @@ -479,16 +501,17 @@ FILESUBTYPE FILESYSPATH Filetime FILEVERSION -Filtergraph Filterkeyboard FILTERMODE -Filterx findfast +Fira FIXEDFILEINFO FIXEDSYS flac +flaticon flyouts FMask +fmtid FOF WANTNUKEWARNING FOFX @@ -500,6 +523,8 @@ FORCEMINIMIZE FORMATDLGORD formatetc FORPARSING +fpvm +Fqc FRAMECHANGED frm Froml @@ -522,8 +547,8 @@ GETDESKWALLPAPER GETDLGCODE GETDPISCALEDSIZE getfilesiginforedist -GETICON GETHOTKEY +GETICON GETMINMAXINFO GETNONCLIENTMETRICS GETPROPERTYSTOREFLAGS @@ -532,6 +557,7 @@ GETSECKEY GETSTICKYKEYS GETTEXTLENGTH GHND +gifv GMEM GNumber gpedit @@ -540,6 +566,7 @@ GPOCA gpp gpu gradians +gsl GSM gtm guiddata @@ -565,7 +592,6 @@ hbr HBRBACKGROUND hbrush hcblack -HCERTSTORE HCRYPTHASH HCRYPTPROV hcursor @@ -591,6 +617,7 @@ HIMAGELIST himl hinst HIWORD +HKC HKCC HKCOMB HKCR @@ -604,6 +631,7 @@ HMD hmenu hmodule hmonitor +homies homljgmgpmcbpjbnjpfijnhipfkiclkd HORZRES HORZSIZE @@ -619,9 +647,11 @@ HREDRAW hres hresult hrgn +HROW hsb HSCROLL hsi +HSSH HTCLIENT hthumbnail HTOUCHINPUT @@ -641,11 +671,13 @@ HWNDPREV hyjiacan IAI IBeam +icf ICONERROR ICONLOCATION idc IDCANCEL IDD +idk idl idlist IDOK @@ -653,15 +685,16 @@ IDR IDXGI ietf IEXPLORE +iextn IFACEMETHOD IFACEMETHODIMP IFile +IGNOREBASECLASS IGNOREUNKNOWN +IGo iid Iindex Ijwhost -IKs -iljxck IMAGEHLP IMAGERESIZERCONTEXTMENU IMAGERESIZEREXT @@ -669,6 +702,8 @@ imageresizerinput imageresizersettings imagingdevices ime +imgflip +inbox INCONTACT Indo inetcpl @@ -678,6 +713,7 @@ Infotip INITDIALOG INITGUID INITTOLOGFONTSTRUCT +INLINEPREFIX inorder INPC inproc @@ -702,6 +738,8 @@ Inste Interlop INTRESOURCE INVALIDARG +INVALIDCALL +INVALIDINDEX invalidoperatioexception ipcmanager IPREVIEW @@ -720,7 +758,6 @@ IUnknown IUse IWIC iwr -IYUV jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jjw @@ -739,6 +776,7 @@ KEYBOARDMANAGEREDITOR KEYBOARDMANAGEREDITORLIBRARYWRAPPER keyboardmanagerstate keyboardmanagerui +keyboardtester KEYEVENTF KEYIMAGE keynum @@ -747,18 +785,15 @@ keyvault KILLFOCUS killrunner kmph -KSPROPERTY Kybd lastcodeanalysissucceeded Lastdevice LASTEXITCODE LAYOUTRTL -lcb LCIDTo Lclean Ldone Ldr -ldx LEFTSCROLLBAR LEFTTEXT LError @@ -766,6 +801,7 @@ LEVELID LExit lhwnd LIBID +libraryincludes LIMITSIZE LIMITTEXT lindex @@ -774,7 +810,6 @@ LINKOVERLAY LINQTo listview LIVEZOOM -lld LLKH llkhf LMEM @@ -782,6 +817,7 @@ LMENU lnks LOADFROMFILE LOBYTE +localappdata LOCALDISPLAY localpackage LOCALSYSTEM @@ -792,7 +828,6 @@ logon LOGPIXELSX LOGPIXELSY longdate -LONGLONG LONGNAMES lowlevel LOWORD @@ -824,10 +859,10 @@ LPTOP lptpm LPTR LPTSTR +lpv LPW lpwcx lpwndpl -lpv LReader LRESULT LSTATUS @@ -836,18 +871,18 @@ lstrcmpi lstrcpyn lstrlen LTEXT -LTRB +LTk LTRREADING luid LUMA +LUQ lusrmgr LVal +lvm LWA lwin LZero MAGTRANSFORM -majortype -makecab MAKEINTRESOURCE MAKEINTRESOURCEA MAKEINTRESOURCEW @@ -863,14 +898,16 @@ MARKDOWNPREVIEWHANDLERCPP MAXIMIZEBOX MAXSHORTCUTSIZE maxversiontested +MBM MBR MDICHILD MDL +mdpvm mdtext mdtxt mdwn -MEDIASUBTYPE -mediatype +meme +memicmp MENUITEMINFO MENUITEMINFOW MERGECOPY @@ -878,9 +915,8 @@ MERGEPAINT Metadatas metafile mfc -mfplat Mgmt -mic +Microwaved midl mii mindaro @@ -890,9 +926,9 @@ MINIMIZEEND MINIMIZESTART miniz MINMAXINFO +minwindef Mip Miracast -mjpg mkdn mlcfg mmc @@ -900,6 +936,7 @@ mmcexe MMdd mmi mmsys +mobileredirect mockapi MODALFRAME MODESPRUNED @@ -927,11 +964,13 @@ msctls msdata MSDL MSGFLT +MSHCTX +MSHLFLAGS +MSIDXS +MSIDXSPROP msiexec MSIFASTINSTALL MSIHANDLE -Msimg -msiquery MSIRESTARTMANAGERCONTROL msixbundle MSIXCA @@ -948,6 +987,7 @@ MULTIPLEUSE multizone muxc mvvm +MVVMTK MWBEx MYICON NAMECHANGE @@ -988,6 +1028,8 @@ NEWPLUSCONTEXTMENU NEWPLUSSHELLEXTENSIONWIN newrow newsgroups +NGQt +nicksnettravels NIF NLog NLSTEXT @@ -1008,6 +1050,7 @@ NOCRLF nodeca NODRAWCAPTION NODRAWICON +NOFIXUPS NOINHERITLAYOUT NOINTERFACE NOINVERT @@ -1021,12 +1064,15 @@ nonclient NONCLIENTMETRICSW NONELEVATED NONINFRINGEMENT +nonspace nonstd +NOOPEN NOOWNERZORDER NOPARENTNOTIFY NOPREFIX NOREDIRECTIONBITMAP NOREDRAW +NOREMAPCLSID NOREMOVE norename NOREPEAT @@ -1045,9 +1091,11 @@ NOTIFYICONDATAW NOTIMPL NOTOPMOST NOTRACK +NOTRUNCATE NOTSRCCOPY NOTSRCERASE NOTXORPEN +NOUSERSETTINGS NOZORDER NPH npmjs @@ -1057,6 +1105,7 @@ NTAPI ntdll NTSTATUS NTSYSAPI +NTZm NULLCURSOR nullonfailure numberbox @@ -1074,12 +1123,12 @@ oldpath oldtheme oleaut OLECHAR +openas opencode OPENFILENAME opensource openxmlformats OPTIMIZEFORINVOKE -ORAW ORPHANEDDIALOGTITLE ORSCANS oss @@ -1090,15 +1139,15 @@ OSVERSIONINFOEXW OSVERSIONINFOW osvi OUTOFCONTEXT -outpin Outptr outsettings OVERLAPPEDWINDOW -overlaywindow Oversampling OVERWRITEPROMPT +OWMt OWNDC OWNERDRAWFIXED +OWRj Packagemanager PACL PAINTSTRUCT @@ -1113,7 +1162,6 @@ PATCOPY PATHMUSTEXIST PATINVERT PATPAINT -PAUDIO pbc pbi PBlob @@ -1154,11 +1202,11 @@ phwnd pici pidl PIDLIST -PINDIR pinfo pinvoke pipename PKBDLLHOOKSTRUCT +Playbadge plib ploc ploca @@ -1168,9 +1216,11 @@ PMAGTRANSFORM PMSIHANDLE pnid PNMLINK -Pnp +Poc +Podcasts POINTERID POINTERUPDATE +Pokedex Popups POPUPWINDOW POSITIONITEM @@ -1183,7 +1233,6 @@ powertoysusersetup Powrprof ppenum ppidl -ppmt pprm pproc ppshv @@ -1198,13 +1247,15 @@ ppv prc Prefixer prependpath +prepopulate prevhost previewer PREVIEWHANDLERFRAMEINFO -previouscamera PREVIOUSINSTALLFOLDER PREVIOUSVERSIONSINSTALLED prevpane +prg +prgh prgms pri PRINTCLIENT @@ -1220,10 +1271,9 @@ PRODUCTVERSION Progman programdata projectname -PROPBAG PROPERTYKEY +Propset PROPVARIANT -propvarutil PRTL prvpane psapi @@ -1249,6 +1299,7 @@ PTOKEN PToy ptstr pui +Puser PWAs pwcs PWSTR @@ -1264,10 +1315,13 @@ Quarternary QUERYENDSESSION QUERYOPEN QUEUESYNC +QUICKTIP QUNS +QXZ RAII RAlt randi +Rasterization Rasterize RAWINPUTDEVICE RAWINPUTHEADER @@ -1286,27 +1340,23 @@ RECTSOURCE recyclebin Redist Reencode -reencoded REFCLSID -REFGUID REFIID REGCLS regfile -REGFILTER -REGFILTERPINS +REGISTERCLASSEX REGISTERCLASSFAILED REGISTRYHEADER registrypath REGISTRYPREVIEWEXT registryroot regkey -REGPINTYPES regroot -regsvr REINSTALLMODE reloadable Relogger remappings +REMAPRUNDLL REMAPSUCCESSFUL REMAPUNSUCCESSFUL Remotable @@ -1333,6 +1383,7 @@ RGBQUAD rgbs rgelt rgf +rgh rgn rgs RIDEV @@ -1343,6 +1394,7 @@ RKey RNumber rop ROUNDSMALL +ROWSETEXT rpcrt RRF rrr @@ -1381,9 +1433,11 @@ SDDL SDKDDK sdns searchterm +searchtext SEARCHUI SECONDARYDISPLAY secpol +SEEMASKINVOKEIDLIST SELCHANGE SENDCHANGE sendvirtualinput @@ -1410,10 +1464,12 @@ settingsheader settingshotkeycontrol setvariable SETWORKAREA +SFBS sfgao SFGAOF SHACF SHANDLE +sharepoint sharpkeys SHCNE SHCNF @@ -1425,14 +1481,17 @@ SHELLDLL shellex SHELLEXECUTEINFO SHELLEXECUTEINFOW +SHELLEXTENSION +SHELLNEWVALUE SHFILEINFO SHFILEOPSTRUCT SHGDN SHGDNF SHGFI +SHGFIICON +SHGFILARGEICON shinfo shlwapi -shmem SHNAMEMAPPING shobjidl SHORTCUTATLEAST @@ -1471,7 +1530,6 @@ Sizename SIZENESW SIZENS SIZENWSE -sizeread SIZEWE SKEXP SKIPOWNPROCESS @@ -1485,6 +1543,8 @@ SNAPPROCESS snwprintf softline SOURCECLIENTAREAONLY +sourced +sourcedoc SOURCEHEADER sourcesdirectory spdisp @@ -1492,6 +1552,8 @@ spdlog spdo spesi splitwstring +Spongebob +spongebot spsi spsia spsrm @@ -1523,15 +1585,15 @@ STATSTG stdafx STDAPI stdc +stdcpp stdcpplatest STDMETHODCALLTYPE STDMETHODIMP STGC STGM STGMEDIUM -sticpl STICKYKEYS -stl +sticpl storelogo stprintf streamjsonrpc @@ -1540,8 +1602,6 @@ stringtable stringval Strm strret -strsafe -strutil stscanf sttngs Stubless @@ -1550,7 +1610,6 @@ STYLECHANGING subkeys sublang SUBMODULEUPDATE -subquery Superbar sut svchost @@ -1565,7 +1624,6 @@ symbolrequestprod SYMCACHE SYMED SYMOPT -SYNCMFT SYNCPAINT SYSCHAR SYSCOLORCHANGE @@ -1621,7 +1679,10 @@ THH THICKFRAME THISCOMPONENT THotkey +throughs +TIcon TILEDWINDOW +TILEINFO TILLSON timedate timediff @@ -1634,6 +1695,7 @@ tkconverters TLayout tlb tlbimp +tlc TPMLEFTALIGN TPMRETURNCMD TMPVAR @@ -1652,10 +1714,12 @@ trafficmanager traies transicc TRAYMOUSEMESSAGE +TResult triaging trl trx tsa +TSender TServer tstoi TStr @@ -1671,6 +1735,8 @@ UAL uap UBR UCallback +ucrt +ucrtd udit uefi uesc @@ -1685,6 +1751,7 @@ UNCPRIORITY UNDNAME unhiding UNICODETEXT +uninstalls Uniquifies unitconverter unittests @@ -1716,18 +1783,17 @@ uxtheme vabdq validmodulename valuegenerator +VARENUM variantassignment vcamp -vcdl +VCENTER vcgtq VCINSTALLDIR Vcpkg VCRT -VCENTER vcruntime vcvars VDesktop -vdi vdupq VERBSONLY VERBW @@ -1738,19 +1804,17 @@ VERTSIZE VFT vget vgetq -vid -VIDCAP -VIDEOINFOHEADER +videourl viewmodel -vih VIRTKEY VIRTUALDESK VISEGRADRELAY visiblecolorformats Visibletrue visualeffects -VKey +vkey vmovl +VMs vorrq VOS vpaddlq @@ -1776,6 +1840,7 @@ vswhere Vtbl WANTMAPPINGHANDLE WANTPALM +wasdk wbem WBounds Wca @@ -1785,17 +1850,20 @@ WClass wcsicmp wcsncpy wcsnicmp +WCT WDA wdm wdp wdupenv webbrowsers -webcam webpage websites wekyb wgpocpl +WHEREID +Wholegrain WIC +wic wifi wil winapi @@ -1830,7 +1898,6 @@ winsta WINTHRESHOLD WINVER winxamlmanager -wistd withinrafael Withscript wixproj @@ -1881,9 +1948,11 @@ wtoi WTS WTSAT Wubi -WVC +WUX Wwanpp XAxis +xclip +xdoc XDocument XElement xfd @@ -1901,13 +1970,16 @@ XUP XVIRTUALSCREEN xxxxxx YAxis +ycombinator Yeet YIncrement yinle yinyue +youtube YPels YResolution YStr +YTM YVIRTUALSCREEN ZEROINIT zonable @@ -1916,4 +1988,6 @@ Zoneszonabletester Zoomin zoomit ZOOMITX -zzz +ZXk +ZXNs +zzz \ No newline at end of file diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index f8c9761933..5a6f4785b1 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -232,6 +232,15 @@ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING # ignore long runs of a single character: \b([A-Za-z])\g{-1}{3,}\b +# Amazon +\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|) + +# imgur +\bimgur\.com/[^.]+ + +# Process Process (typename varname) +Process Process + # ZoomIt menu items with accelerator keys E&xit -St&yle +St&yle \ No newline at end of file diff --git a/.gitignore b/.gitignore index 89541c3a2e..66532cc074 100644 --- a/.gitignore +++ b/.gitignore @@ -224,7 +224,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -322,7 +322,7 @@ ImageResizer/tools/** # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -331,7 +331,7 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder .mfractor/ # Temp build files diff --git a/.pipelines/ESRPSigning_cmdpal_msix_content.json b/.pipelines/ESRPSigning_cmdpal_msix_content.json new file mode 100644 index 0000000000..6522fc56b3 --- /dev/null +++ b/.pipelines/ESRPSigning_cmdpal_msix_content.json @@ -0,0 +1,51 @@ +{ + "Version": "1.0.0", + "UseMinimatch": false, + "SignBatches": [ + { + "MatchedPath": [ + "*.dll", + "*.exe" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } + ] +} diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 7b9af0a767..1b78856032 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -217,7 +217,10 @@ "PowerToys.ZoomItSettingsInterop.dll", "WinUI3Apps\\PowerToys.Settings.dll", - "WinUI3Apps\\PowerToys.Settings.exe" + "WinUI3Apps\\PowerToys.Settings.exe", + + "PowerToys.CmdPalModuleInterface.dll", + "*Microsoft.CmdPal.UI_*.msix" ], "SigningInfo": { "Operations": [ diff --git a/.pipelines/ESRPSigning_sdk.json b/.pipelines/ESRPSigning_sdk.json new file mode 100644 index 0000000000..066acf9e4e --- /dev/null +++ b/.pipelines/ESRPSigning_sdk.json @@ -0,0 +1,51 @@ +{ + "Version": "1.0.0", + "UseMinimatch": false, + "SignBatches": [ + { + "MatchedPath": [ + "Microsoft.CommandPalette.Extensions.dll", + "Microsoft.CommandPalette.Extensions.Toolkit.dll" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } + ] +} diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index 3e0669bfd2..26819c5d14 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -20,6 +20,16 @@ parameters: type: string default: '0.0.1' + - name: cmdPalVersionNumber + displayName: "Command Palette Version Number" + type: string + default: '0.0.1' + + - name: cmdPalSdkVersionNumber + displayName: "Command Palette SDK Version Number" + type: string + default: '0.0.1' + - name: buildConfigurations displayName: "Build Configurations" type: object @@ -78,6 +88,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} buildConfigurations: ${{ parameters.buildConfigurations }} versionNumber: ${{ parameters.versionNumber }} + cmdPalVersionNumber: ${{ parameters.cmdPalVersionNumber }} publishArtifacts: false # 1ES PT handles publication for us. codeSign: true runTests: false @@ -95,7 +106,7 @@ extends: beforeBuildSteps: # Sets versions for all PowerToy created DLLs - pwsh: |- - .pipelines/versionSetting.ps1 -versionNumber '${{ parameters.versionNumber }}' -DevEnvironment '' + .pipelines/versionSetting.ps1 -versionNumber '${{ parameters.versionNumber }}' -DevEnvironment '' -cmdPalVersionNumber '${{ parameters.cmdPalVersionNumber }}' displayName: Prepare versioning # Prepare the localizations and telemetry config before the release build @@ -107,6 +118,28 @@ extends: move /Y "Microsoft.PowerToys.Telemetry.2.0.2\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs" || exit /b 1 displayName: Emplace telemetry files + - stage: Build_SDK + displayName: Build SDK + dependsOn: [] + jobs: + - template: .pipelines/v2/templates/job-build-sdk.yml@self + parameters: + pool: + name: SHINE-INT-L + image: SHINE-VS17-Latest + os: windows + codeSign: true + sdkVersionNumber: ${{ parameters.cmdPalSdkVersionNumber }} + signingIdentity: + serviceName: $(SigningServiceName) + appId: $(SigningAppId) + tenantId: $(SigningTenantId) + akvName: $(SigningAKVName) + authCertName: $(SigningAuthCertName) + signCertName: $(SigningSignCertName) + useManagedIdentity: $(SigningUseManagedIdentity) + clientId: $(SigningOriginalClientId) + - stage: Publish displayName: Publish dependsOn: [Build] diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index 481534b687..6014d5b992 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -56,6 +56,9 @@ parameters: - name: versionNumber type: string default: '0.0.1' + - name: cmdPalVersionNumber + type: string + default: '0.0.1' - name: useLatestWinAppSDK type: boolean default: false @@ -96,6 +99,7 @@ jobs: ${{ else }}: OutputBuildPlatform: ${{ platform }} variables: + MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe' # Azure DevOps abhors a vacuum # If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names* # later on. We'll just... set them to a single space and if we need to, check IsNullOrWhiteSpace. @@ -237,6 +241,32 @@ jobs: env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: Stage SDK/build + inputs: + contents: |- + "**/cmdpal/extensionsdk/nuget/Microsoft.CommandPalette.Extensions.SDK.props" + "**/cmdpal/extensionsdk/nuget/Microsoft.CommandPalette.Extensions.SDK.targets" + flattenFolders: True + targetFolder: $(JobOutputDirectory)/sdk/build + + - task: CopyFiles@2 + displayName: Stage SDK/lib + inputs: + contents: |- + "**/Microsoft.CommandPalette.Extensions.Toolkit/$(BuildPlatform)/release/WinUI3Apps/CmdPal/Microsoft.CommandPalette.Extensions.Toolkit.dll" + "**/Microsoft.CommandPalette.Extensions.Toolkit/$(BuildPlatform)/release/WinUI3Apps/CmdPal/Microsoft.CommandPalette.Extensions.Toolkit.deps.json" + flattenFolders: True + targetFolder: $(JobOutputDirectory)/sdk/lib/net8.0-windows10.0.19041.0 + + - task: CopyFiles@2 + displayName: Stage SDK/winmd + inputs: + contents: |- + "**/Microsoft.CommandPalette.Extensions/$(BuildPlatform)/release/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.winmd" + flattenFolders: True + targetFolder: $(JobOutputDirectory)/sdk/winmd + - task: VSBuild@1 displayName: Build BugReportTool inputs: @@ -304,7 +334,7 @@ jobs: displayName: HACK Copy core WebView2 ARM64 dll to output directory condition: eq(variables['BuildPlatform'],'arm64') inputs: - contents: packages/Microsoft.Web.WebView2.1.0.2739.15/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll + contents: packages/Microsoft.Web.WebView2.1.0.2903.40/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll targetFolder: $(Build.SourcesDirectory)/ARM64/Release/WinUI3Apps/ flattenFolders: True OverWrite: True @@ -355,6 +385,33 @@ jobs: !**\obj\** - ${{ if eq(parameters.codeSign, true) }}: + - pwsh: |- + $Package = (Get-ChildItem -Recurse -Filter "Microsoft.CmdPal.UI_*.msix" | Select -First 1) + $PackageFilename = $Package.FullName + Write-Host "##vso[task.setvariable variable=CmdPalPackagePath]${PackageFilename}" + displayName: Locate the MSIX + + - pwsh: |- + & "$(MakeAppxPath)" unpack /p "$(CmdPalPackagePath)" /d "$(JobOutputDirectory)/CmdPalPackageContents" + displayName: Unpack the MSIX for signing + + - template: steps-esrp-signing.yml + parameters: + displayName: Sign CmdPal MSIX content + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: '$(JobOutputDirectory)/CmdPalPackageContents' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_cmdpal_msix_content.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - pwsh: |- + $outDir = New-Item -Type Directory "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore + $PackageFilename = Join-Path $outDir.FullName (Split-Path -Leaf "$(CmdPalPackagePath)") + & "$(MakeAppxPath)" pack /h SHA256 /o /p $PackageFilename /d "$(JobOutputDirectory)/CmdPalPackageContents" + Copy-Item -Force $PackageFilename "$(CmdPalPackagePath)" + displayName: Re-pack the new CmdPal package after signing + - template: steps-esrp-signing.yml parameters: displayName: Sign Core PowerToys @@ -430,7 +487,7 @@ jobs: $machinePlat = "hash_machine_$(BuildPlatform).txt"; $combinedUserPath = $p + $userPlat; $combinedMachinePath = $p + $machinePlat; - + echo $p echo $userPlat @@ -440,7 +497,7 @@ jobs: echo $machinePlat echo $machineHash echo $combinedMachinePath - + $userHash | out-file -filepath $combinedUserPath $machineHash | out-file -filepath $combinedMachinePath displayName: Calculate file hashes diff --git a/.pipelines/v2/templates/job-build-sdk.yml b/.pipelines/v2/templates/job-build-sdk.yml new file mode 100644 index 0000000000..f8aa5dca3e --- /dev/null +++ b/.pipelines/v2/templates/job-build-sdk.yml @@ -0,0 +1,91 @@ +parameters: + - name: buildConfigurations + type: object + default: + - Release + - name: codeSign + type: boolean + default: false + - name: pool + type: object + default: [] + - name: signingIdentity + type: object + default: {} + - name: sdkVersionNumber + type: string + default: '0.0.1' + +jobs: +- job: "BuildSDK" + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + displayName: Build SDK + timeoutInMinutes: 240 + cancelTimeoutInMinutes: 1 + templateContext: # Required when this template is hosted in 1ES PT + outputs: + - output: pipelineArtifact + artifactName: SDK + targetPath: $(Build.ArtifactStagingDirectory) + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + fetchTags: false + fetchDepth: 1 + + - pwsh: |- + & "$(build.sourcesdirectory)\src\modules\cmdpal\extensionsdk\nuget\BuildSDKHelper.ps1" -Configuration "Release" -VersionOfSDK ${{ parameters.sdkVersionNumber }} -BuildStep "build" -IsAzurePipelineBuild + displayName: Build SDK + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: Sign SDK + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'src/modules' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_sdk.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - pwsh: |- + & "$(build.sourcesdirectory)\src\modules\cmdpal\extensionsdk\nuget\BuildSDKHelper.ps1" -Configuration "Release" -VersionOfSDK ${{ parameters.sdkVersionNumber }} -BuildStep "pack" -IsAzurePipelineBuild + displayName: Pack SDK + + - task: CopyFiles@2 + displayName: Copy Nuget to Artifact Staging + inputs: + sourceFolder: "$(build.sourcesdirectory)/src/modules/cmdpal/extensionsdk/_build" + contents: '*.nupkg' + targetFolder: '$(Build.ArtifactStagingDirectory)' + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: Sign NuGet packages + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: $(Build.ArtifactStagingDirectory) + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml index d5252ba23b..234f477fb6 100644 --- a/.pipelines/v2/templates/job-test-project.yml +++ b/.pipelines/v2/templates/job-test-project.yml @@ -105,4 +105,4 @@ jobs: **\UITests-FancyZones.dll **\UITests-FancyZonesEditor.dll !**\obj\** - !**\ref\** + !**\ref\** \ No newline at end of file diff --git a/.pipelines/v2/templates/steps-build-installer.yml b/.pipelines/v2/templates/steps-build-installer.yml index a4eb61481c..8c3c89dbc0 100644 --- a/.pipelines/v2/templates/steps-build-installer.yml +++ b/.pipelines/v2/templates/steps-build-installer.yml @@ -87,6 +87,30 @@ steps: dir $(build.sourcesdirectory)\extractedMsi displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Extract and verify MSI" + # Extract CmdPal msix package to check if its content is signed + - pwsh: |- + Write-Host "Extracting CmdPal MSIX package" + + # Define the directory to search + $searchDir = "extractedMsi\File" + + # Define the regex pattern for MSIX files + $pattern = '^Microsoft.CmdPal.UI.*\.msix$' + + # Get all files in the directory and subdirectories + $msixFile = Get-ChildItem -Path $searchDir -Recurse -File | Where-Object { + $_.Name -match $pattern + } + + Write-Host "MSIX file found: " $msixFile + + $destinationDir = "$(build.sourcesdirectory)\extractedMsi\File\extractedCmdPalMsix" + + Expand-Archive -Path $msixFile -DestinationPath $destinationDir + Get-ChildItem -Path $destinationDir -Recurse -File + + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Extract CmdPal MSIX package + # Check if deps.json files don't reference different dll versions. - pwsh: |- & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File' diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1 index e4f1733ac6..e85ca1d991 100644 --- a/.pipelines/verifyDepsJsonLibraryVersions.ps1 +++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1 @@ -18,6 +18,11 @@ $totalFailures = 0 Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object { # Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies $depsJsonFullFileName = $_.FullName + + if ($depsJsonFullFileName -like "*CmdPal*") { + return + } + $depsJsonFileName = $_.Name $depsJson = Get-Content $depsJsonFullFileName | ConvertFrom-Json diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1 index 1970bd6adb..89cfbeea1c 100644 --- a/.pipelines/versionAndSignCheck.ps1 +++ b/.pipelines/versionAndSignCheck.ps1 @@ -22,7 +22,11 @@ $versionExceptions = @( "TraceReloggerLib.dll", "Microsoft.WindowsAppRuntime.Release.Net.dll", "Microsoft.Windows.Widgets.Projection.dll", - "WinRT.Host.Shim.dll") -join '|'; + "WinRT.Host.Shim.dll", + "WyHash.dll", + "Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll", + "ObjectModelCsProjection.dll", + "RendererCsProjection.dll") -join '|'; $nullVersionExceptions = @( "codicon.ttf", "e_sqlite3.dll", @@ -43,12 +47,14 @@ $nullVersionExceptions = @( "PushNotificationsLongRunningTask.ProxyStub.dll", "WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll", "System.Diagnostics.EventLog.Messages.dll", - "Microsoft.Windows.Widgets.dll") -join '|'; + "Microsoft.Windows.Widgets.dll", + "AdaptiveCards.ObjectModel.WinUI3.dll", + "AdaptiveCards.Rendering.WinUI3.dll") -join '|'; $totalFailure = 0; Write-Host $DirPath; -if (-not (Test-Path $DirPath)) { +if (-not (Test-Path $DirPath)) { Write-Error "Folder does not exist!" } @@ -70,7 +76,7 @@ $items | ForEach-Object { Write-Host "Version set to 1.0.0.0: " + $_.FullName $totalFailure++; } - elseif ($_.VersionInfo.FileVersion -eq $null -and $_.Name -notmatch $nullVersionExceptions) { + elseif ($_.VersionInfo.FileVersion -eq $null -and $_.Name -notmatch $nullVersionExceptions) { # These items are exceptions that actually a version not set. Write-Host "Version not set: " + $_.FullName $totalFailure++; diff --git a/.pipelines/versionSetting.ps1 b/.pipelines/versionSetting.ps1 index 7221392195..bda3c47cc2 100644 --- a/.pipelines/versionSetting.ps1 +++ b/.pipelines/versionSetting.ps1 @@ -5,7 +5,10 @@ Param( [Parameter(Mandatory=$True,Position=2)] [AllowEmptyString()] - [string]$DevEnvironment = "Local" + [string]$DevEnvironment = "Local", + + [Parameter(Mandatory=$True,Position=3)] + [string]$cmdPalVersionNumber = "0.0.1" ) Write-Host $PSScriptRoot @@ -38,9 +41,20 @@ $verPropReadFileLocation = $verPropWriteFileLocation; $verProps.Project.PropertyGroup.Version = $versionNumber; $verProps.Project.PropertyGroup.DevEnvironment = $DevEnvironment; -Write-Host "xml" $verProps.Project.PropertyGroup.Version +Write-Host "xml" $verProps.Project.PropertyGroup.Version $verProps.Save($verPropWriteFileLocation); + +#### The same thing as above, but for the CmdPal version +$verPropWriteFileLocation = $PSScriptRoot + '/../src/CmdPalVersion.props'; +$verPropReadFileLocation = $verPropWriteFileLocation; +[XML]$verProps = Get-Content $verPropReadFileLocation +$verProps.Project.PropertyGroup.CmdPalVersion = $cmdPalVersionNumber; +$verProps.Project.PropertyGroup.DevEnvironment = $DevEnvironment; +Write-Host "xml" $verProps.Project.PropertyGroup.Version +$verProps.Save($verPropWriteFileLocation); +####### + # Set PowerRenameContextMenu package version in AppManifest.xml $powerRenameContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src/modules/powerrename/PowerRenameContextMenu/AppxManifest.xml'; $powerRenameContextMenuAppManifestReadFileLocation = $powerRenameContextMenuAppManifestWriteFileLocation; @@ -76,3 +90,12 @@ $newPlusContextMenuAppManifestReadFileLocation = $newPlusContextMenuAppManifestW $newPlusContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0' Write-Host "NewPlusContextMenu version" $newPlusContextMenuAppManifest.Package.Identity.Version $newPlusContextMenuAppManifest.Save($newPlusContextMenuAppManifestWriteFileLocation); + +# Set package version in Package.appxmanifest +$cmdPalAppManifestWriteFileLocation = $PSScriptRoot + '/../src/modules/cmdpal/Microsoft.CmdPal.UI/Package.appxmanifest'; +$cmdPalAppManifestReadFileLocation = $cmdPalAppManifestWriteFileLocation; + +[XML]$cmdPalAppManifest = Get-Content $cmdPalAppManifestReadFileLocation +$cmdPalAppManifest.Package.Identity.Version = $cmdPalVersionNumber + '.0' +Write-Host "CmdPal Package version: " $cmdPalAppManifest.Package.Identity.Version +$cmdPalAppManifest.Save($cmdPalAppManifestWriteFileLocation); diff --git a/.vsconfig b/.vsconfig index bc5f1200fc..77ec8b0ffd 100644 --- a/.vsconfig +++ b/.vsconfig @@ -1,12 +1,13 @@ { "version": "1.0", - "components": [ + "components": [ "Microsoft.VisualStudio.Component.CoreEditor", "Microsoft.VisualStudio.Workload.CoreEditor", "Microsoft.VisualStudio.Workload.NativeDesktop", "Microsoft.VisualStudio.Workload.ManagedDesktop", "Microsoft.VisualStudio.Workload.Universal", "Microsoft.VisualStudio.Component.Windows10SDK.19041", + "Microsoft.VisualStudio.Component.Windows10SDK.20348", "Microsoft.VisualStudio.Component.Windows10SDK.22621", "Microsoft.VisualStudio.ComponentGroup.UWP.VC", "Microsoft.VisualStudio.Component.UWP.VC.ARM64", @@ -18,4 +19,4 @@ "Microsoft.VisualStudio.Component.VC.ATL.Spectre", "Microsoft.VisualStudio.ComponentGroup.WindowsAppSDK.Cs" ] -} \ No newline at end of file +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 9698a4adb7..091364bd78 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,17 +3,21 @@ true + + + - - - - - - - - - + + + + + + + + + + @@ -38,9 +42,10 @@ - + + @@ -92,6 +97,7 @@ + diff --git a/NOTICE.md b/NOTICE.md index 138743ecfe..0b9cbb5fc2 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1323,6 +1323,43 @@ EXHIBIT A -Mozilla Public License. Original Code Source Code for Your Modifications.] ``` +## Utility: Command Palette + +### wyhash + +We use the WyHash NuGet package for calculating stable hashes for strings. + +**Source**: [https://github.com/wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash) + +### License + +``` +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +``` + ## Utility: Registry Preview ### Monaco Editor @@ -1355,17 +1392,22 @@ SOFTWARE. ## NuGet Packages used by PowerToys + +- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta +- AdaptiveCards.Rendering.WinUI3 2.1.0-beta +- AdaptiveCards.Templating 2.0.2 - Appium.WebDriver 4.4.5 - Azure.AI.OpenAI 1.0.0-beta.17 -- CommunityToolkit.Mvvm 8.2.2 -- CommunityToolkit.WinUI.Animations 8.0.240109 -- CommunityToolkit.WinUI.Collections 8.0.240109 -- CommunityToolkit.WinUI.Controls.Primitives 8.0.240109 -- CommunityToolkit.WinUI.Controls.Segmented 8.0.240109 -- CommunityToolkit.WinUI.Controls.SettingsControls 8.0.240109 -- CommunityToolkit.WinUI.Controls.Sizers 8.0.240109 -- CommunityToolkit.WinUI.Converters 8.0.240109 -- CommunityToolkit.WinUI.Extensions 8.0.240109 +- CommunityToolkit.Common 8.4.0 +- CommunityToolkit.Mvvm 8.4.0 +- CommunityToolkit.WinUI.Animations 8.2.250129-preview2 +- CommunityToolkit.WinUI.Collections 8.2.250129-preview2 +- CommunityToolkit.WinUI.Controls.Primitives 8.2.250129-preview2 +- CommunityToolkit.WinUI.Controls.Segmented 8.2.250129-preview2 +- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250129-preview2 +- CommunityToolkit.WinUI.Controls.Sizers 8.2.250129-preview2 +- CommunityToolkit.WinUI.Converters 8.2.250129-preview2 +- CommunityToolkit.WinUI.Extensions 8.2.250129-preview2 - CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2 - CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2 - ControlzEx 6.0.0 @@ -1390,13 +1432,14 @@ SOFTWARE. - Microsoft.NET.ILLink.Tasks (A) - Microsoft.SemanticKernel 1.15.0 - Microsoft.Toolkit.Uwp.Notifications 7.1.2 -- Microsoft.Web.WebView2 1.0.2739.15 +- Microsoft.Web.WebView2 1.0.2903.40 - Microsoft.Win32.SystemEvents 9.0.3 - Microsoft.Windows.Compatibility 9.0.3 - Microsoft.Windows.CsWin32 0.2.46-beta - Microsoft.Windows.CsWinRT 2.2.0 - Microsoft.Windows.SDK.BuildTools 10.0.22621.2428 - Microsoft.WindowsAppSDK 1.6.250205002 +- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview - Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9 - Microsoft.Xaml.Behaviors.Wpf 1.1.39 - ModernWpfUI 0.9.4 @@ -1432,3 +1475,4 @@ SOFTWARE. - UTF.Unknown 2.5.1 - WinUIEx 2.2.0 - WPF-UI 3.0.5 +- WyHash 1.0.5 diff --git a/PowerToys.sln b/PowerToys.sln index 04b276b361..9b911b388b 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -620,10 +620,64 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EtwTrace", "src\common\Tele EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBorders.UnitTests", "src\modules\MouseWithoutBorders\MouseWithoutBorders.UnitTests\MouseWithoutBorders.UnitTests.csproj", "{66614C26-314C-4B91-9071-76133422CFEF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPalette", "CommandPalette", "{3846508C-77EB-4034-A702-F8BB263C4F79}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Built-in Extensions", "Built-in Extensions", "{ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extension SDK", "Extension SDK", "{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.CommandPalette.Extensions", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.vcxproj", "{305DD37E-C85D-4B08-AAFE-7381FA890463}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommandPalette.Extensions.Toolkit", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj", "{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Common", "src\modules\cmdpal\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\modules\cmdpal\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample Extensions", "Sample Extensions", "{071E18A4-A530-46B8-AB7D-B862EE55E24E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\Exts\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\Exts\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{7520A2FE-00A2-49B8-83ED-DB216E874C04}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "src\modules\cmdpal\Microsoft.CmdPal.UI\Microsoft.CmdPal.UI.csproj", "{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{C66020D1-CB10-4CF7-8715-84C97FD5E5E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalModuleInterface", "src\modules\cmdpal\CmdPalModuleInterface\CmdPalModuleInterface.vcxproj", "{0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "src\modules\Workspaces\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj", "{89D0E199-B17A-418C-B2F8-7375B6708357}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "src\modules\AdvancedPaste\AdvancedPaste.FuzzTests\AdvancedPaste.FuzzTests.csproj", "{7F5B9557-5878-4438-A721-3E28296BA193}" @@ -636,6 +690,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "sr EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyboardManagerEditorUI", "src\modules\keyboardmanager\KeyboardManagerEditorUI\KeyboardManagerEditorUI.csproj", "{08F9155D-B6DC-46E5-9C83-AF60B655898B}" @@ -648,6 +704,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\module EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests", "src\modules\registrypreview\RegistryPreview.FuzzTests\RegistryPreview.FuzzTests.csproj", "{5702B3CC-8575-48D5-83D8-15BB42269CD3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2200,6 +2258,154 @@ Global {66614C26-314C-4B91-9071-76133422CFEF}.Release|ARM64.Build.0 = Release|ARM64 {66614C26-314C-4B91-9071-76133422CFEF}.Release|x64.ActiveCfg = Release|x64 {66614C26-314C-4B91-9071-76133422CFEF}.Release|x64.Build.0 = Release|x64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|ARM64.Build.0 = Debug|ARM64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|x64.ActiveCfg = Debug|x64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|x64.Build.0 = Debug|x64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|ARM64.ActiveCfg = Release|ARM64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|ARM64.Build.0 = Release|ARM64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|x64.ActiveCfg = Release|x64 + {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|x64.Build.0 = Release|x64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|ARM64.Build.0 = Debug|ARM64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|x64.ActiveCfg = Debug|x64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|x64.Build.0 = Debug|x64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|ARM64.ActiveCfg = Release|ARM64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|ARM64.Build.0 = Release|ARM64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.ActiveCfg = Release|x64 + {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.Build.0 = Release|x64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.Build.0 = Debug|ARM64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.ActiveCfg = Debug|x64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.Build.0 = Debug|x64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.ActiveCfg = Release|ARM64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.Build.0 = Release|ARM64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.ActiveCfg = Release|x64 + {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.Build.0 = Release|x64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.Build.0 = Debug|ARM64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|x64.ActiveCfg = Debug|x64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|x64.Build.0 = Debug|x64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|ARM64.ActiveCfg = Release|ARM64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|ARM64.Build.0 = Release|ARM64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|x64.ActiveCfg = Release|x64 + {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|x64.Build.0 = Release|x64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|ARM64.Build.0 = Debug|ARM64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|x64.ActiveCfg = Debug|x64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|x64.Build.0 = Debug|x64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|ARM64.ActiveCfg = Release|ARM64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|ARM64.Build.0 = Release|ARM64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|x64.ActiveCfg = Release|x64 + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|x64.Build.0 = Release|x64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|ARM64.Build.0 = Debug|ARM64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|x64.ActiveCfg = Debug|x64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|x64.Build.0 = Debug|x64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Release|ARM64.ActiveCfg = Release|ARM64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Release|ARM64.Build.0 = Release|ARM64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Release|x64.ActiveCfg = Release|x64 + {D1160404-D3D1-497A-883A-4059C07C2273}.Release|x64.Build.0 = Release|x64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|ARM64.Build.0 = Debug|ARM64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|x64.ActiveCfg = Debug|x64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|x64.Build.0 = Debug|x64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|ARM64.ActiveCfg = Release|ARM64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|ARM64.Build.0 = Release|ARM64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|x64.ActiveCfg = Release|x64 + {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|x64.Build.0 = Release|x64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM64.Build.0 = Debug|ARM64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.ActiveCfg = Debug|x64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.Build.0 = Debug|x64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM64.ActiveCfg = Release|ARM64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM64.Build.0 = Release|ARM64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.ActiveCfg = Release|x64 + {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.Build.0 = Release|x64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|x64.ActiveCfg = Debug|x64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|x64.Build.0 = Debug|x64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|ARM64.Build.0 = Release|ARM64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|x64.ActiveCfg = Release|x64 + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|x64.Build.0 = Release|x64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|ARM64.Build.0 = Debug|ARM64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|x64.ActiveCfg = Debug|x64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|x64.Build.0 = Debug|x64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|ARM64.ActiveCfg = Release|ARM64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|ARM64.Build.0 = Release|ARM64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|x64.ActiveCfg = Release|x64 + {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|x64.Build.0 = Release|x64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|ARM64.Build.0 = Debug|ARM64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|x64.ActiveCfg = Debug|x64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|x64.Build.0 = Debug|x64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|ARM64.ActiveCfg = Release|ARM64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|ARM64.Build.0 = Release|ARM64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x64.ActiveCfg = Release|x64 + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x64.Build.0 = Release|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.Build.0 = Debug|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.ActiveCfg = Debug|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.Build.0 = Debug|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.Deploy.0 = Debug|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.ActiveCfg = Release|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.Build.0 = Release|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.Deploy.0 = Release|ARM64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.ActiveCfg = Release|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.Build.0 = Release|x64 + {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.Deploy.0 = Release|x64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.Build.0 = Debug|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.ActiveCfg = Debug|x64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.Build.0 = Debug|x64 + {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.Deploy.0 = Debug|x64 + {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.ActiveCfg = Release|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.Build.0 = Release|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.Deploy.0 = Release|ARM64 + {C831231F-891C-4572-9694-45062534B42A}.Release|x64.ActiveCfg = Release|x64 + {C831231F-891C-4572-9694-45062534B42A}.Release|x64.Build.0 = Release|x64 + {C831231F-891C-4572-9694-45062534B42A}.Release|x64.Deploy.0 = Release|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.Build.0 = Debug|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.ActiveCfg = Debug|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.Build.0 = Debug|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.Deploy.0 = Debug|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.ActiveCfg = Release|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.Build.0 = Release|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.Deploy.0 = Release|ARM64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.ActiveCfg = Release|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.Build.0 = Release|x64 + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.Deploy.0 = Release|x64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|ARM64.Build.0 = Debug|ARM64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|x64.ActiveCfg = Debug|x64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|x64.Build.0 = Debug|x64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|ARM64.ActiveCfg = Release|ARM64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|ARM64.Build.0 = Release|ARM64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x64.ActiveCfg = Release|x64 + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x64.Build.0 = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.Build.0 = Debug|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.ActiveCfg = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.Build.0 = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.ActiveCfg = Release|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.Build.0 = Release|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.ActiveCfg = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.Build.0 = Release|x64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.Build.0 = Debug|ARM64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|x64.ActiveCfg = Debug|x64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|x64.Build.0 = Debug|x64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|ARM64.ActiveCfg = Release|ARM64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|ARM64.Build.0 = Release|ARM64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|x64.ActiveCfg = Release|x64 + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|x64.Build.0 = Release|x64 {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.ActiveCfg = Debug|ARM64 {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.Build.0 = Debug|ARM64 {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.ActiveCfg = Debug|x64 @@ -2216,6 +2422,54 @@ Global {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|ARM64.Build.0 = Release|ARM64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.ActiveCfg = Release|x64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Build.0 = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.ActiveCfg = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Build.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Deploy.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.ActiveCfg = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Build.0 = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Deploy.0 = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.ActiveCfg = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Deploy.0 = Release|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|ARM64.Build.0 = Debug|ARM64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x64.ActiveCfg = Debug|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x64.Build.0 = Debug|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|ARM64.ActiveCfg = Release|ARM64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|ARM64.Build.0 = Release|ARM64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x64.ActiveCfg = Release|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x64.Build.0 = Release|x64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.Build.0 = Debug|ARM64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|x64.ActiveCfg = Debug|x64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|x64.Build.0 = Debug|x64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|ARM64.ActiveCfg = Release|ARM64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|ARM64.Build.0 = Release|ARM64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|x64.ActiveCfg = Release|x64 + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|x64.Build.0 = Release|x64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|ARM64.Build.0 = Debug|ARM64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|x64.ActiveCfg = Debug|x64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|x64.Build.0 = Debug|x64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|ARM64.ActiveCfg = Release|ARM64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|ARM64.Build.0 = Release|ARM64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x64.ActiveCfg = Release|x64 + {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x64.Build.0 = Release|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.Build.0 = Debug|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.ActiveCfg = Debug|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.Build.0 = Debug|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.Deploy.0 = Debug|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.ActiveCfg = Release|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.Build.0 = Release|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.Deploy.0 = Release|ARM64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.ActiveCfg = Release|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.Build.0 = Release|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.Deploy.0 = Release|x64 {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.ActiveCfg = Debug|ARM64 {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.Build.0 = Debug|ARM64 {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x64.ActiveCfg = Debug|x64 @@ -2256,6 +2510,18 @@ Global {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64 {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64 {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.Build.0 = Debug|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.ActiveCfg = Debug|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.Build.0 = Debug|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.Deploy.0 = Debug|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.ActiveCfg = Release|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.Build.0 = Release|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.Deploy.0 = Release|ARM64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.ActiveCfg = Release|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.Build.0 = Release|x64 + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.Deploy.0 = Release|x64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.Build.0 = Debug|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.ActiveCfg = Debug|x64 @@ -2304,6 +2570,18 @@ Global {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|ARM64.Build.0 = Release|ARM64 {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|x64.ActiveCfg = Release|x64 {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|x64.Build.0 = Release|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.ActiveCfg = Debug|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.Build.0 = Debug|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.ActiveCfg = Release|x64 + {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2531,20 +2809,49 @@ Global {37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {8F021B46-362B-485C-BFBA-CCF83E820CBD} = {8F62026A-294B-41C6-8839-87463613F216} {66614C26-314C-4B91-9071-76133422CFEF} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} + {3846508C-77EB-4034-A702-F8BB263C4F79} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} = {3846508C-77EB-4034-A702-F8BB263C4F79} + {6CE438DF-C245-4997-A360-0A0939E4BA34} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {E09AA983-C755-474F-83D6-A5CDF528C070} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {6D56B64D-FF1F-488F-AFED-9B9854A5D399} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {92EC89E4-9972-453A-8A1A-3A9E230C146A} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {51939B4F-1F62-4BFF-A6A2-C08646E5BE95} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {D1160404-D3D1-497A-883A-4059C07C2273} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {40F6D69D-E321-400F-A767-5628C7AE453D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} = {3846508C-77EB-4034-A702-F8BB263C4F79} + {305DD37E-C85D-4B08-AAFE-7381FA890463} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} + {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} + {14E62033-58D0-4A7D-8990-52F50A08BBBD} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} + {071E18A4-A530-46B8-AB7D-B862EE55E24E} = {3846508C-77EB-4034-A702-F8BB263C4F79} + {C846F7A7-792A-47D9-B0CB-417C900EE03D} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} + {C831231F-891C-4572-9694-45062534B42A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} + {7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79} + {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} + {C66020D1-CB10-4CF7-8715-84C97FD5E5E2} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} + {79775343-7A3D-445D-9104-3DD5B2893DF9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8} = {3846508C-77EB-4034-A702-F8BB263C4F79} {89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {C0CE3B5E-16D3-495D-B335-CA791B660162} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {605E914B-7232-4789-AF46-BF5D3DDFC14E} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE} = {9873BA05-4C41-4819-9283-CF45D795431B} {7F5B9557-5878-4438-A721-3E28296BA193} = {9873BA05-4C41-4819-9283-CF45D795431B} {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {0A84F764-3A88-44CD-AA96-41BDBD48627B} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} {E4585179-2AC1-4D5F-A3FF-CFC5392F694C} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} {CA7D8106-30B9-4AEC-9D05-B69B31B8C461} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} + {DCC6BD67-17BB-47AA-B507-FB0FE43A7449} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} {A558C25D-2007-498E-8B6F-43405AFAE9E2} = {1AFB6476-670D-4E80-A464-657E01DFF482} {08F9155D-B6DC-46E5-9C83-AF60B655898B} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} {5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5} + {64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/installer/PowerToysSetup/CmdPal.wxs b/installer/PowerToysSetup/CmdPal.wxs new file mode 100644 index 0000000000..89a813979b --- /dev/null +++ b/installer/PowerToysSetup/CmdPal.wxs @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/PowerToysBootstrapper.wixproj b/installer/PowerToysSetup/PowerToysBootstrapper.wixproj index 15a2000ca5..b2f5945dc2 100644 --- a/installer/PowerToysSetup/PowerToysBootstrapper.wixproj +++ b/installer/PowerToysSetup/PowerToysBootstrapper.wixproj @@ -17,6 +17,12 @@ $(DefineConstants);PerUser=false + + $(DefineConstants);CIBuild=true + + + $(DefineConstants);CIBuild=false + Release x64 diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj index 7ce39b82cf..f76d15b73a 100644 --- a/installer/PowerToysSetup/PowerToysInstaller.wixproj +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -1,9 +1,11 @@ - + + - Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC + Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion) Release @@ -104,6 +113,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + @@ -188,4 +198,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil - \ No newline at end of file + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 33dc8d0e55..2c99d84044 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -79,6 +79,10 @@ + + + + @@ -135,6 +139,7 @@ + @@ -150,6 +155,9 @@ NOT Installed + + NOT Installed + @@ -204,6 +212,10 @@ Property="LaunchPowerToys" Value="[INSTALLFOLDER]" /> + + + + diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index d0aca611fd..84c86af124 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -11,6 +11,7 @@ #include "../../src/common/updating/installer.h" #include "../../src/common/version/version.h" #include "../../src/common/Telemetry/EtwTrace/EtwTrace.h" +#include "../../src/common/utils/package.h" #include "../../src/common/utils/clean_video_conference.h" #include @@ -35,13 +36,13 @@ TRACELOGGING_DEFINE_PROVIDER( TraceLoggingOptionProjectTelemetry()); const DWORD USERNAME_DOMAIN_LEN = DNLEN + UNLEN + 2; // Domain Name + '\' + User Name + '\0' -const DWORD USERNAME_LEN = UNLEN + 1; // User Name + '\0' +const DWORD USERNAME_LEN = UNLEN + 1; // User Name + '\0' -static const wchar_t* POWERTOYS_EXE_COMPONENT = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}"; -static const wchar_t* POWERTOYS_UPGRADE_CODE = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}"; +static const wchar_t *POWERTOYS_EXE_COMPONENT = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}"; +static const wchar_t *POWERTOYS_UPGRADE_CODE = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}"; -constexpr inline const wchar_t* DataDiagnosticsRegKey = L"Software\\Classes\\PowerToys"; -constexpr inline const wchar_t* DataDiagnosticsRegValueName = L"AllowDataDiagnostics"; +constexpr inline const wchar_t *DataDiagnosticsRegKey = L"Software\\Classes\\PowerToys"; +constexpr inline const wchar_t *DataDiagnosticsRegValueName = L"AllowDataDiagnostics"; #define TraceLoggingWriteWrapper(provider, eventName, ...) \ if (isDataDiagnosticEnabled()) \ @@ -52,16 +53,16 @@ constexpr inline const wchar_t* DataDiagnosticsRegValueName = L"AllowDataDiagnos trace.UpdateState(false); \ } -static Shared::Trace::ETWTrace trace{ L"PowerToys_Installer" }; +static Shared::Trace::ETWTrace trace{L"PowerToys_Installer"}; inline bool isDataDiagnosticEnabled() { HKEY key{}; if (RegOpenKeyExW(HKEY_CURRENT_USER, - DataDiagnosticsRegKey, - 0, - KEY_READ, - &key) != ERROR_SUCCESS) + DataDiagnosticsRegKey, + 0, + KEY_READ, + &key) != ERROR_SUCCESS) { return false; } @@ -86,8 +87,7 @@ inline bool isDataDiagnosticEnabled() return isDataDiagnosticsEnabled == 1; } - -HRESULT getInstallFolder(MSIHANDLE hInstall, std::wstring& installationDir) +HRESULT getInstallFolder(MSIHANDLE hInstall, std::wstring &installationDir) { DWORD len = 0; wchar_t _[1]; @@ -116,13 +116,13 @@ BOOL IsLocalSystem() // open process token if (!OpenProcessToken(GetCurrentProcess(), - TOKEN_QUERY, - &hToken)) + TOKEN_QUERY, + &hToken)) return FALSE; // retrieve user SID if (!GetTokenInformation(hToken, TokenUser, pTokenUser, - sizeof(bTokenUser), &cbTokenUser)) + sizeof(bTokenUser), &cbTokenUser)) { CloseHandle(hToken); return FALSE; @@ -132,7 +132,7 @@ BOOL IsLocalSystem() // allocate LocalSystem well-known SID if (!AllocateAndInitializeSid(&siaNT, 1, SECURITY_LOCAL_SYSTEM_RID, - 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) + 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) return FALSE; // compare the user SID from the token with the LocalSystem SID @@ -194,7 +194,7 @@ static std::filesystem::path GetUserPowerShellModulesPath() if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &myDocumentsBlockPtr))) { - const std::wstring myDocuments{ myDocumentsBlockPtr }; + const std::wstring myDocuments{myDocumentsBlockPtr}; CoTaskMemFree(myDocumentsBlockPtr); return std::filesystem::path(myDocuments) / "PowerShell" / "Modules"; } @@ -227,10 +227,12 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall) BOOL isSystemUser = IsLocalSystem(); - if (isSystemUser) { + if (isSystemUser) + { - auto action = [&commandLine](HANDLE userToken) { - STARTUPINFO startupInfo{ .cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL }; + auto action = [&commandLine](HANDLE userToken) + { + STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL}; PROCESS_INFORMATION processInformation; PVOID lpEnvironment = NULL; @@ -269,7 +271,7 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall) } else { - STARTUPINFO startupInfo{ .cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL }; + STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL}; PROCESS_INFORMATION processInformation; @@ -313,7 +315,7 @@ UINT __stdcall CheckGPOCA(MSIHANDLE hInstall) LPWSTR currentScope = nullptr; hr = WcaGetProperty(L"InstallScope", ¤tScope); - if (std::wstring{ currentScope } == L"perUser") + if (std::wstring{currentScope} == L"perUser") { if (powertoys_gpo::getDisablePerUserInstallationValue() == powertoys_gpo::gpo_rule_configured_enabled) { @@ -354,7 +356,7 @@ UINT __stdcall ApplyModulesRegistryChangeSetsCA(MSIHANDLE hInstall) hr = getInstallFolder(hInstall, installationFolder); ExitOnFailure(hr, "Failed to get installFolder."); - for (const auto& changeSet : getAllOnByDefaultModulesChangeSets(installationFolder)) + for (const auto &changeSet : getAllOnByDefaultModulesChangeSets(installationFolder)) { if (!changeSet.apply()) { @@ -382,7 +384,7 @@ UINT __stdcall UnApplyModulesRegistryChangeSetsCA(MSIHANDLE hInstall) ExitOnFailure(hr, "Failed to initialize"); hr = getInstallFolder(hInstall, installationFolder); ExitOnFailure(hr, "Failed to get installFolder."); - for (const auto& changeSet : getAllModulesChangeSets(installationFolder)) + for (const auto &changeSet : getAllModulesChangeSets(installationFolder)) { changeSet.unApply(); } @@ -396,8 +398,8 @@ LExit: return WcaFinalize(er); } -const wchar_t* DSC_CONFIGURE_PSD1_NAME = L"Microsoft.PowerToys.Configure.psd1"; -const wchar_t* DSC_CONFIGURE_PSM1_NAME = L"Microsoft.PowerToys.Configure.psm1"; +const wchar_t *DSC_CONFIGURE_PSD1_NAME = L"Microsoft.PowerToys.Configure.psd1"; +const wchar_t *DSC_CONFIGURE_PSM1_NAME = L"Microsoft.PowerToys.Configure.psm1"; UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall) { @@ -429,7 +431,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall) ExitOnFailure(hr, "Unable to create Powershell modules folder"); } - for (const auto* filename : { DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME }) + for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME}) { fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode); @@ -477,7 +479,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall) std::error_code errorCode; - for (const auto* filename : { DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME }) + for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME}) { fs::remove(versionedModulePath / filename, errorCode); @@ -488,7 +490,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall) } } - for (const auto* modulePath : { &versionedModulePath, &powerToysModulePath }) + for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath}) { fs::remove(*modulePath, errorCode); @@ -535,7 +537,7 @@ UINT __stdcall InstallEmbeddedMSIXCA(MSIHANDLE hInstall) using namespace winrt::Windows::Management::Deployment; using namespace winrt::Windows::Foundation; - Uri msix_uri{ msix_path.wstring() }; + Uri msix_uri{msix_path.wstring()}; PackageManager pm; auto result = pm.AddPackageAsync(msix_uri, nullptr, DeploymentOptions::None).get(); if (!result) @@ -569,7 +571,7 @@ UINT __stdcall UninstallEmbeddedMSIXCA(MSIHANDLE hInstall) hr = WcaInitialize(hInstall, "UninstallEmbeddedMSIXCA"); ExitOnFailure(hr, "Failed to initialize"); - for (const auto& p : pm.FindPackagesForUser({}, package_name, publisher)) + for (const auto &p : pm.FindPackagesForUser({}, package_name, publisher)) { auto result = pm.RemovePackageAsync(p.Id().FullName()).get(); if (result) @@ -683,7 +685,6 @@ UINT __stdcall UninstallCommandNotFoundModuleCA(MSIHANDLE hInstall) command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(installationFolder) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\""; #endif - system(command.c_str()); LExit: @@ -738,10 +739,10 @@ UINT __stdcall RemoveScheduledTasksCA(MSIHANDLE hInstall) HRESULT hr = S_OK; UINT er = ERROR_SUCCESS; - ITaskService* pService = nullptr; - ITaskFolder* pTaskFolder = nullptr; - IRegisteredTaskCollection* pTaskCollection = nullptr; - ITaskFolder* pRootFolder = nullptr; + ITaskService *pService = nullptr; + ITaskFolder *pTaskFolder = nullptr; + IRegisteredTaskCollection *pTaskCollection = nullptr; + ITaskFolder *pRootFolder = nullptr; LONG numTasks = 0; hr = WcaInitialize(hInstall, "RemoveScheduledTasksCA"); @@ -754,10 +755,10 @@ UINT __stdcall RemoveScheduledTasksCA(MSIHANDLE hInstall) // ------------------------------------------------------ // Create an instance of the Task Service. hr = CoCreateInstance(CLSID_TaskScheduler, - nullptr, - CLSCTX_INPROC_SERVER, - IID_ITaskService, - reinterpret_cast(&pService)); + nullptr, + CLSCTX_INPROC_SERVER, + IID_ITaskService, + reinterpret_cast(&pService)); ExitOnFailure(hr, "Failed to create an instance of ITaskService: %x", hr); // Connect to the task service. @@ -785,7 +786,7 @@ UINT __stdcall RemoveScheduledTasksCA(MSIHANDLE hInstall) { // Delete all the tasks found. // If some tasks can't be deleted, the folder won't be deleted later and the user will still be notified. - IRegisteredTask* pRegisteredTask = nullptr; + IRegisteredTask *pRegisteredTask = nullptr; hr = pTaskCollection->get_Item(_variant_t(i + 1), &pRegisteredTask); if (SUCCEEDED(hr)) { @@ -861,8 +862,7 @@ UINT __stdcall TelemetryLogInstallSuccessCA(MSIHANDLE hInstall) TraceLoggingWideString(get_product_version().c_str(), "Version"), ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"), - TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE) - ); + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); LExit: er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; @@ -1028,7 +1028,7 @@ UINT __stdcall DetectPrevInstallPathCA(MSIHANDLE hInstall) try { - if (auto install_path = GetMsiPackageInstalledPath(std::wstring{ currentScope } == L"perUser")) + if (auto install_path = GetMsiPackageInstalledPath(std::wstring{currentScope} == L"perUser")) { MsiSetPropertyW(hInstall, L"PREVIOUSINSTALLFOLDER", install_path->data()); } @@ -1040,6 +1040,47 @@ UINT __stdcall DetectPrevInstallPathCA(MSIHANDLE hInstall) return WcaFinalize(er); } +UINT __stdcall InstallCmdPalPackageCA(MSIHANDLE hInstall) +{ + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::Management::Deployment; + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + std::wstring installationFolder; + + hr = WcaInitialize(hInstall, "InstallCmdPalPackage"); + hr = getInstallFolder(hInstall, installationFolder); + + try + { + auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\", false); + auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\Dependencies\\", true); + + if (!msix.empty()) + { + auto msixPath = msix[0]; + + if (!package::RegisterPackage(msixPath, dependencies)) + { + Logger::error(L"Failed to install CmdPal package"); + er = ERROR_INSTALL_FAILURE; + } + } + } + catch (std::exception &e) + { + std::string errorMessage{"Exception thrown while trying to install CmdPal package: "}; + errorMessage += e.what(); + Logger::error(errorMessage); + + er = ERROR_INSTALL_FAILURE; + } + + er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er; + return WcaFinalize(er); +} + UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall) { using namespace winrt::Windows::Foundation; @@ -1053,54 +1094,20 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall) try { // Packages to unregister - const std::vector packagesToRemoveDisplayName{ { L"PowerRenameContextMenu" }, { L"ImageResizerContextMenu" }, { L"FileLocksmithContextMenu" }, { L"NewPlusContextMenu" } }; + const std::vector packagesToRemoveDisplayName{{L"PowerRenameContextMenu"}, {L"ImageResizerContextMenu"}, {L"FileLocksmithContextMenu"}, {L"NewPlusContextMenu"}, {L"Microsoft.CommandPalette"}}; - PackageManager packageManager; - - for (auto const& package : packageManager.FindPackages()) + for (auto const &package : packagesToRemoveDisplayName) { - const auto& packageFullName = std::wstring{ package.Id().FullName() }; - - for (const auto& packageToRemove : packagesToRemoveDisplayName) + if (!package::UnRegisterPackage(package)) { - if (packageFullName.contains(packageToRemove)) - { - auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) }; - deploymentOperation.get(); - - // Check the status of the operation - if (deploymentOperation.Status() == AsyncStatus::Error) - { - auto deploymentResult{ deploymentOperation.GetResults() }; - auto errorCode = deploymentOperation.ErrorCode(); - auto errorText = deploymentResult.ErrorText(); - - Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText); - - er = ERROR_INSTALL_FAILURE; - } - else if (deploymentOperation.Status() == AsyncStatus::Canceled) - { - Logger::error(L"Unregister {} package canceled.", packageFullName); - - er = ERROR_INSTALL_FAILURE; - } - else if (deploymentOperation.Status() == AsyncStatus::Completed) - { - Logger::info(L"Unregister {} package completed.", packageFullName); - } - else - { - Logger::debug(L"Unregister {} package started.", packageFullName); - } - } - + Logger::error(L"Failed to unregister package: " + package); + er = ERROR_INSTALL_FAILURE; } } } - catch (std::exception& e) + catch (std::exception &e) { - std::string errorMessage{ "Exception thrown while trying to unregister sparse packages: " }; + std::string errorMessage{"Exception thrown while trying to unregister sparse packages: "}; errorMessage += e.what(); Logger::error(errorMessage); @@ -1128,7 +1135,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array processesToTerminate = { L"PowerToys.PowerLauncher.exe", L"PowerToys.Settings.exe", L"PowerToys.AdvancedPaste.exe", @@ -1165,6 +1172,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.WorkspacesLauncherUI.exe", L"PowerToys.WorkspacesEditor.exe", L"PowerToys.WorkspacesWindowArranger.exe", + L"Microsoft.CmdPal.UI.exe", L"PowerToys.ZoomIt.exe", L"PowerToys.exe", }; @@ -1177,7 +1185,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } wchar_t processName[MAX_PATH] = L""; - HANDLE hProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, FALSE, procID) }; + HANDLE hProcess{OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, FALSE, procID)}; if (!hProcess) { continue; @@ -1197,8 +1205,9 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) if (processName == processToTerminate) { const DWORD timeout = 500; - auto windowEnumerator = [](HWND hwnd, LPARAM procIDPtr) -> BOOL { - auto targetProcID = *reinterpret_cast(procIDPtr); + auto windowEnumerator = [](HWND hwnd, LPARAM procIDPtr) -> BOOL + { + auto targetProcID = *reinterpret_cast(procIDPtr); DWORD windowProcID = 0; GetWindowThreadProcessId(hwnd, &windowProcID); if (windowProcID == targetProcID) @@ -1224,15 +1233,15 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) void initSystemLogger() { static std::once_flag initLoggerFlag; - std::call_once(initLoggerFlag, []() { - WCHAR temp_path[MAX_PATH]; - auto ret = GetTempPath(MAX_PATH, temp_path); + std::call_once(initLoggerFlag, []() + { + WCHAR temp_path[MAX_PATH]; + auto ret = GetTempPath(MAX_PATH, temp_path); - if (ret) - { - Logger::init("PowerToysMSI", std::wstring{ temp_path } + L"\\PowerToysMSIInstaller", L""); - } - }); + if (ret) + { + Logger::init("PowerToysMSI", std::wstring{ temp_path } + L"\\PowerToysMSIInstaller", L""); + } }); } // DllMain - Initialize and cleanup WiX custom action utils. diff --git a/installer/PowerToysSetupCustomActions/CustomAction.def b/installer/PowerToysSetupCustomActions/CustomAction.def index d9ed0d0f04..e91060b764 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.def +++ b/installer/PowerToysSetupCustomActions/CustomAction.def @@ -18,6 +18,7 @@ EXPORTS TerminateProcessesCA InstallEmbeddedMSIXCA InstallDSCModuleCA + InstallCmdPalPackageCA UnApplyModulesRegistryChangeSetsCA UnRegisterContextMenuPackagesCA UninstallEmbeddedMSIXCA diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj index e2de4a4065..09ed1ee31a 100644 --- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj +++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj @@ -1,5 +1,6 @@  - + @@ -54,6 +55,7 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\AdvancedPaste.wxs"" ""$(ProjectDir)..\PowerToysSetup\AdvancedPaste.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Awake.wxs"" ""$(ProjectDir)..\PowerToysSetup\Awake.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs"" ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs.bk"""" + call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\CmdPal.wxs"" ""$(ProjectDir)..\PowerToysSetup\CmdPal.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Core.wxs"" ""$(ProjectDir)..\PowerToysSetup\Core.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs.bk"""" diff --git a/src/CmdPalVersion.props b/src/CmdPalVersion.props new file mode 100644 index 0000000000..e9e0e98130 --- /dev/null +++ b/src/CmdPalVersion.props @@ -0,0 +1,10 @@ + + + + 0.0.1 + Local + + + SHA256 + + diff --git a/src/Common.Dotnet.AotCompatibility.props b/src/Common.Dotnet.AotCompatibility.props index 9c9b3faa25..71c490fd6c 100644 --- a/src/Common.Dotnet.AotCompatibility.props +++ b/src/Common.Dotnet.AotCompatibility.props @@ -5,5 +5,8 @@ true true 2 + + + IL2081 diff --git a/src/Common.Dotnet.CsWinRT.props b/src/Common.Dotnet.CsWinRT.props index f3c8c25ecc..e4731ce2fd 100644 --- a/src/Common.Dotnet.CsWinRT.props +++ b/src/Common.Dotnet.CsWinRT.props @@ -14,7 +14,7 @@ 4 True - CA1720;CA1859;CA2263;CA2022 + CA1720;CA1859;CA2263;CA2022;MVVMTK0045;MVVMTK0049 diff --git a/src/codeAnalysis/GlobalSuppressions.cs b/src/codeAnalysis/GlobalSuppressions.cs index 64e1ab9b16..c05e5f8820 100644 --- a/src/codeAnalysis/GlobalSuppressions.cs +++ b/src/codeAnalysis/GlobalSuppressions.cs @@ -12,10 +12,10 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "We follow the C# Core Coding Style which avoids using `this` unless absolutely necessary.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields", Justification = "It is not a priority and have hight impact in code changes.")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "It is not a priority and have hight impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "It is not a priority and has high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "It is not a priority and has high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1203:ConstantsMustAppearBeforeFields", Justification = "It is not a priority and has high impact in code changes.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "It is not a priority and has high impact in code changes.")] [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:FieldNamesMustNotBeginWithUnderscore", Justification = "We follow the C# Core Coding Style which uses underscores as prefixes rather than using `this.`.")] @@ -62,3 +62,9 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] + +// AOT +[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "HostsUILib")] +[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI")] +[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI.Views")] +[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "type", Target = "~T:Peek.UI.Views.TitleBar")] diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs index bb26fa71e6..d247e726a1 100644 --- a/src/common/Common.UI/SettingsDeepLink.cs +++ b/src/common/Common.UI/SettingsDeepLink.cs @@ -31,6 +31,7 @@ namespace Common.UI Dashboard, AdvancedPaste, Workspaces, + CmdPal, ZoomIt, } @@ -78,6 +79,8 @@ namespace Common.UI return "AdvancedPaste"; case SettingsWindow.Workspaces: return "Workspaces"; + case SettingsWindow.CmdPal: + return "CmdPal"; case SettingsWindow.ZoomIt: return "ZoomIt"; default: diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 691a66b43f..62b5b49a9d 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -16,6 +16,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredCmdNotFoundEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredCmdPalEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredCmdPalEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredColorPickerEnabledValue() { return static_cast(powertoys_gpo::getConfiguredColorPickerEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index f3eb07680b..0d7783883b 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -10,6 +10,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue(); + static GpoRuleConfigured GetConfiguredCmdPalEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index b7aa8e22aa..1e3c3a19f5 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -14,6 +14,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue(); + static GpoRuleConfigured GetConfiguredCmdPalEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index 5b95af43d8..65b00d4b5a 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -10,6 +10,7 @@ namespace ManagedCommon AlwaysOnTop, Awake, ColorPicker, + CmdPal, CropAndLock, EnvironmentVariables, FancyZones, diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp index 144fb728ce..62e6425f6f 100644 --- a/src/common/interop/Constants.cpp +++ b/src/common/interop/Constants.cpp @@ -187,4 +187,8 @@ namespace winrt::PowerToys::Interop::implementation { return CommonSharedConstants::TERMINATE_SETTINGS_SHARED_EVENT; } + hstring Constants::ShowCmdPalEvent() + { + return CommonSharedConstants::CMDPAL_SHOW_EVENT; + } } diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h index b2a5fdef53..1b3a0f556c 100644 --- a/src/common/interop/Constants.h +++ b/src/common/interop/Constants.h @@ -50,6 +50,7 @@ namespace winrt::PowerToys::Interop::implementation static hstring WorkspacesLaunchEditorEvent(); static hstring WorkspacesHotkeyEvent(); static hstring PowerToysRunnerTerminateSettingsEvent(); + static hstring ShowCmdPalEvent(); }; } diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl index e2a356d5ef..1de4b849ab 100644 --- a/src/common/interop/Constants.idl +++ b/src/common/interop/Constants.idl @@ -47,6 +47,7 @@ namespace PowerToys static String WorkspacesLaunchEditorEvent(); static String WorkspacesHotkeyEvent(); static String PowerToysRunnerTerminateSettingsEvent(); + static String ShowCmdPalEvent(); } } } \ No newline at end of file diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 1c4808ad6a..6853973806 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -128,6 +128,9 @@ namespace CommonSharedConstants const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324"; const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220"; + // used from quick access window + const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; + // Max DWORD for key code to disable keys. const DWORD VK_DISABLED = 0x100; } diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 479fb0ef23..5d79fa3b01 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -4,8 +4,10 @@ #include #include -namespace powertoys_gpo { - enum gpo_rule_configured_t { +namespace powertoys_gpo +{ + enum gpo_rule_configured_t + { gpo_rule_configured_wrong_value = -3, // The policy is set to an unrecognized value gpo_rule_configured_unavailable = -2, // Couldn't access registry gpo_rule_configured_not_configured = -1, // Policy is not configured @@ -53,6 +55,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_SHORTCUT_GUIDE = L"ConfigureEnabledUtilityShortcutGuide"; const std::wstring POLICY_CONFIGURE_ENABLED_TEXT_EXTRACTOR = L"ConfigureEnabledUtilityTextExtractor"; const std::wstring POLICY_CONFIGURE_ENABLED_ADVANCED_PASTE = L"ConfigureEnabledUtilityAdvancedPaste"; + const std::wstring POLICY_CONFIGURE_ENABLED_CMD_PAL = L"ConfigureEnabledUtilityCmdPal"; const std::wstring POLICY_CONFIGURE_ENABLED_ZOOM_IT = L"ConfigureEnabledUtilityZoomIt"; const std::wstring POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW = L"ConfigureEnabledUtilityRegistryPreview"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_WITHOUT_BORDERS = L"ConfigureEnabledUtilityMouseWithoutBorders"; @@ -157,16 +160,17 @@ namespace powertoys_gpo { machine_key_found = false; } - if(machine_key_found) + if (machine_key_found) { // If the path was found in the machine, we need to check if the value for the policy exists. auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast(&value), &valueSize); RegCloseKey(key); - if (res != ERROR_SUCCESS) { + if (res != ERROR_SUCCESS) + { // Value not found on the path. - machine_key_found=false; + machine_key_found = false; } } @@ -175,7 +179,8 @@ namespace powertoys_gpo { // If there's no value found on the machine scope, try to get it from the user scope. if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS) { - if (res == ERROR_FILE_NOT_FOUND) { + if (res == ERROR_FILE_NOT_FOUND) + { return gpo_rule_configured_not_configured; } return gpo_rule_configured_unavailable; @@ -183,7 +188,8 @@ namespace powertoys_gpo { auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast(&value), &valueSize); RegCloseKey(key); - if (res != ERROR_SUCCESS) { + if (res != ERROR_SUCCESS) + { return gpo_rule_configured_not_configured; } } @@ -412,6 +418,11 @@ namespace powertoys_gpo { return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ADVANCED_PASTE); } + inline gpo_rule_configured_t getConfiguredCmdPalEnabledValue() + { + return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CMD_PAL); + } + inline gpo_rule_configured_t getConfiguredWorkspacesEnabledValue() { return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_WORKSPACES); @@ -502,7 +513,7 @@ namespace powertoys_gpo { } inline gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID) - { + { if (pluginID == "" || pluginID == " ") { // this plugin id can't exist in the registry @@ -511,7 +522,7 @@ namespace powertoys_gpo { std::wstring plugin_id(pluginID.begin(), pluginID.end()); auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id); - + if (individual_plugin_setting.has_value()) { if (*individual_plugin_setting == L"0") @@ -538,7 +549,7 @@ namespace powertoys_gpo { { // If no individual plugin policy exists, we check the policy with the setting for all plugins. return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS); - } + } } inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue() @@ -602,7 +613,7 @@ namespace powertoys_gpo { } else { - return std::wstring (); + return std::wstring(); } } diff --git a/src/common/utils/package.h b/src/common/utils/package.h index 7ca6ac37aa..4ca9bafd25 100644 --- a/src/common/utils/package.h +++ b/src/common/utils/package.h @@ -3,7 +3,10 @@ #include #include +#include +#include #include +#include #include #include @@ -13,6 +16,11 @@ #include "../version/version.h" namespace package { + + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::ApplicationModel; + using namespace winrt::Windows::Management::Deployment; + inline BOOL IsWin11OrGreater() { OSVERSIONINFOEX osvi{}; @@ -38,35 +46,35 @@ namespace package { dwlConditionMask); } - inline bool IsPackageRegistered(std::wstring packageDisplayName) + inline std::optional GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion) { - using namespace winrt::Windows::Foundation; - using namespace winrt::Windows::Management::Deployment; - PackageManager packageManager; - for (auto const& package : packageManager.FindPackagesForUser({})) + for (const auto& package : packageManager.FindPackagesForUser({})) { const auto& packageFullName = std::wstring{ package.Id().FullName() }; const auto& packageVersion = package.Id().Version(); if (packageFullName.contains(packageDisplayName)) { - if (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION) + // If checkVersion is true, verify if the package has the same version as PowerToys. + if ((!checkVersion) || (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION)) { - return true; + return { package }; } } } - return false; + return {}; } - inline bool RegisterSparsePackage(std::wstring externalLocation, std::wstring sparsePkgPath) + inline bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName) { - using namespace winrt::Windows::Foundation; - using namespace winrt::Windows::Management::Deployment; + return GetRegisteredPackage(packageDisplayName, true).has_value(); + } + inline bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath) + { try { Uri externalUri{ externalLocation }; @@ -115,4 +123,174 @@ namespace package { return false; } } + + inline bool UnRegisterPackage(const std::wstring& pkgDisplayName) + { + try + { + PackageManager packageManager; + const static auto packages = packageManager.FindPackages(); + + for (auto const& package : packages) + { + const auto& packageFullName = std::wstring{ package.Id().FullName() }; + + if (packageFullName.contains(pkgDisplayName)) + { + auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) }; + deploymentOperation.get(); + + // Check the status of the operation + if (deploymentOperation.Status() == AsyncStatus::Error) + { + auto deploymentResult{ deploymentOperation.GetResults() }; + auto errorCode = deploymentOperation.ErrorCode(); + auto errorText = deploymentResult.ErrorText(); + + Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText); + } + else if (deploymentOperation.Status() == AsyncStatus::Canceled) + { + Logger::error(L"Unregister {} package canceled.", packageFullName); + } + else if (deploymentOperation.Status() == AsyncStatus::Completed) + { + Logger::info(L"Unregister {} package completed.", packageFullName); + } + else + { + Logger::debug(L"Unregister {} package started.", packageFullName); + } + + break; + } + } + } + catch (std::exception& e) + { + Logger::error("Exception thrown while trying to unregister package: {}", e.what()); + return false; + } + + return true; + } + + inline std::vector FindMsixFile(const std::wstring& directoryPath, bool recursive) + { + if (directoryPath.empty()) + { + return {}; + } + + if (!std::filesystem::exists(directoryPath)) + { + Logger::error(L"The directory '" + directoryPath + L"' does not exist."); + } + + const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase); + std::vector matchedFiles; + + try + { + if (recursive) + { + for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath)) + { + if (entry.is_regular_file()) + { + const auto& fileName = entry.path().filename().string(); + if (std::regex_match(fileName, pattern)) + { + matchedFiles.push_back(entry.path()); + } + } + } + } + else + { + for (const auto& entry : std::filesystem::directory_iterator(directoryPath)) + { + if (entry.is_regular_file()) + { + const auto& fileName = entry.path().filename().string(); + if (std::regex_match(fileName, pattern)) + { + matchedFiles.push_back(entry.path()); + } + } + } + } + } + catch (const std::exception& ex) + { + Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what())); + } + + return matchedFiles; + } + + inline bool RegisterPackage(std::wstring pkgPath, std::vector dependencies) + { + try + { + Uri packageUri{ pkgPath }; + + PackageManager packageManager; + + // Declare use of an external location + DeploymentOptions options = DeploymentOptions::ForceApplicationShutdown; + + Collections::IVector uris = winrt::single_threaded_vector(); + if (!dependencies.empty()) + { + for (const auto& dependency : dependencies) + { + try + { + uris.Append(Uri(dependency)); + } + catch (const winrt::hresult_error& ex) + { + Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str()); + } + } + } + + IAsyncOperationWithProgress deploymentOperation = packageManager.AddPackageAsync(packageUri, uris, options); + deploymentOperation.get(); + + // Check the status of the operation + if (deploymentOperation.Status() == AsyncStatus::Error) + { + auto deploymentResult{ deploymentOperation.GetResults() }; + auto errorCode = deploymentOperation.ErrorCode(); + auto errorText = deploymentResult.ErrorText(); + + Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(errorCode), errorText); + return false; + } + else if (deploymentOperation.Status() == AsyncStatus::Canceled) + { + Logger::error(L"Register {} package canceled.", pkgPath); + return false; + } + else if (deploymentOperation.Status() == AsyncStatus::Completed) + { + Logger::info(L"Register {} package completed.", pkgPath); + } + else + { + Logger::debug(L"Register {} package started.", pkgPath); + } + + } + catch (std::exception& e) + { + Logger::error("Exception thrown while trying to register package: {}", e.what()); + + return false; + } + + return true; + } } \ No newline at end of file diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 7702b99b98..ba54f2728a 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -1,11 +1,11 @@ - + - + @@ -25,6 +25,7 @@ + @@ -106,6 +107,16 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index 4a68d0069f..ca48535b3d 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -1,7 +1,7 @@ - + PowerToys PowerToys @@ -32,6 +32,7 @@ PowerToys version 0.86.0 or later PowerToys version 0.88.0 or later PowerToys version 0.89.0 or later + PowerToys version 0.90.0 or later From PowerToys version 0.64.0 until PowerToys version 0.87.1 This policy configures the enabled state for all PowerToys utilities. @@ -242,6 +243,7 @@ If you don't configure this policy, the user will be able to control the setting Awake: Configure enabled state Color Picker: Configure enabled state Command Not Found: Configure enabled state + CmdPal: Configure enabled state Crop And Lock: Configure enabled state Environment Variables: Configure enabled state FancyZones: Configure enabled state diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj index b67ebf2880..fba18de07c 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj @@ -69,6 +69,10 @@ + + + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj index 7ee87d33a2..2dcbc1e237 100644 --- a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj @@ -2,7 +2,7 @@ - + WinExe EnvironmentVariables @@ -67,6 +67,10 @@ + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp index 78808a9428..ec755d99a3 100644 --- a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp +++ b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp @@ -83,7 +83,7 @@ public: std::wstring path = get_module_folderpath(globals::instance); std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix"; - if (!package::IsPackageRegistered(constants::nonlocalizable::ContextMenuPackageName)) + if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName)) { package::RegisterSparsePackage(path, packageUri); } diff --git a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj index a39daec324..f4b28d3922 100644 --- a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj +++ b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj @@ -63,6 +63,10 @@ + + + + diff --git a/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj b/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj index 8d332f9ea5..0f0a57ba6b 100644 --- a/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj +++ b/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj @@ -20,6 +20,8 @@ runtime + + diff --git a/src/modules/Hosts/Hosts/Hosts.csproj b/src/modules/Hosts/Hosts/Hosts.csproj index 7cfaecfb97..cf595dd44b 100644 --- a/src/modules/Hosts/Hosts/Hosts.csproj +++ b/src/modules/Hosts/Hosts/Hosts.csproj @@ -66,6 +66,10 @@ + + + + diff --git a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs index 42371e3361..60a036bdb8 100644 --- a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs +++ b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs @@ -202,7 +202,9 @@ namespace HostsUILib.ViewModels } _entries.CollectionChanged += Entries_CollectionChanged; +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code Entries = new AdvancedCollectionView(_entries, true); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending)); ApplyFilters(); OnPropertyChanged(nameof(Entries)); diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj index 226a1d6112..86d258854f 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj @@ -140,7 +140,7 @@ - + @@ -152,7 +152,7 @@ - + diff --git a/src/modules/MeasureTool/MeasureToolCore/packages.config b/src/modules/MeasureTool/MeasureToolCore/packages.config index f391108479..61ff4b9f07 100644 --- a/src/modules/MeasureTool/MeasureToolCore/packages.config +++ b/src/modules/MeasureTool/MeasureToolCore/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj index 8a34e06024..434ff088b2 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj @@ -13,7 +13,7 @@ app.manifest true x64;ARM64 - true + true false false true @@ -55,6 +55,8 @@ + + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h index 580555f8a6..50f92562d2 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h @@ -155,7 +155,7 @@ namespace newplus::utilities static const auto new_dll_path = get_module_folderpath(module_instance_handle); auto new_package_uri = new_dll_path + L"\\" + constants::non_localizable::msix_package_name; - if (!package::IsPackageRegistered(constants::non_localizable::context_menu_package_name)) + if (!package::IsPackageRegisteredWithPowerToysVersion(constants::non_localizable::context_menu_package_name)) { package::RegisterSparsePackage(new_dll_path, new_package_uri); } diff --git a/src/modules/cmdpal/.wt.json b/src/modules/cmdpal/.wt.json new file mode 100644 index 0000000000..230329e876 --- /dev/null +++ b/src/modules/cmdpal/.wt.json @@ -0,0 +1,31 @@ +{ + "$version": "1.0.0", + "snippets": + [ + { + "input": "pwsh -c .\\doc\\initial-sdk-spec\\generate-interface.ps1 > .\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.idl", + "name": "Generate interface", + "description": "Generate the interface from the SDK spec\nThis drops it into Microsoft.CommandPalette.Extensions" + }, + { + "input": "tasklist | findstr Extension", + "name": "List running extensions", + "description": "This will list all running extensions, as long as they have 'Extension' in the name (they should)" + }, + { + "input": "for /F \"tokens=2\" %A in ('tasklist ^| findstr Extension') do taskkill /PID %A /F", + "name": "🚨 Terminate extensions 🚨", + "description": "Terminate anything with 'Extension' in the name" + }, + { + "input": "start https://github.com/zadjii-msft/PowerToys/compare/main...zadjii-msft:PowerToys:{branch}?expand=1\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D", + "name": "New PR", + "description": "Create a new PR targeting the right fork.\nReplace {branch} with the actual branch you want to merge." + }, + { + "input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd", + "name": "Update template project", + "description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory." + } + ] +} diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.rc b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.rc new file mode 100644 index 0000000000..5fa3c8b90d --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.rc @@ -0,0 +1,40 @@ +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#include "winres.h" +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj new file mode 100644 index 0000000000..4395e340fa --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj @@ -0,0 +1,87 @@ + + + + + 17.0 + Win32Proj + {0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8} + CmdPalModuleInterface + 10.0 + PowerToys.CmdPalModuleInterface + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\ + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + + + + EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + + + + Create + + + + + + + + + + + + + + + + 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}. + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj.filters b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj.filters new file mode 100644 index 0000000000..1b2723105d --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + Source Files + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..419e66c648 --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp @@ -0,0 +1,268 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +HINSTANCE g_hInst_cmdPal = 0; + +BOOL APIENTRY DllMain(HMODULE hInstance, + DWORD ul_reason_for_call, + LPVOID) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + g_hInst_cmdPal = hInstance; + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +class CmdPal : public PowertoyModuleIface +{ +private: + bool m_enabled = false; + + std::wstring app_name; + + //contains the non localized key of the powertoy + std::wstring app_key; + + void LaunchApp() + { + auto package = package::GetRegisteredPackage(L"Microsoft.CommandPalette", false); + + if (package.has_value()) + { + auto getAppListEntriesOperation = package->GetAppListEntriesAsync(); + auto appEntries = getAppListEntriesOperation.get(); + + if (appEntries.Size() > 0) + { + winrt::Windows::Foundation::IAsyncOperation launchOperation = appEntries.GetAt(0).LaunchAsync(); + launchOperation.get(); + } + else + { + Logger::error(L"No app entries found for the package."); + } + } + else + { + Logger::error(L"CmdPal package is not registered."); + } + } + + std::vector GetProcessesIdByName(const std::wstring& processName) + { + std::vector processIds; + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snapshot != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32 processEntry; + processEntry.dwSize = sizeof(PROCESSENTRY32); + + if (Process32First(snapshot, &processEntry)) + { + do + { + if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0) + { + processIds.push_back(processEntry.th32ProcessID); + } + } while (Process32Next(snapshot, &processEntry)); + } + + CloseHandle(snapshot); + } + + return processIds; + } + + void TerminateCmdPal() + { + auto processIds = GetProcessesIdByName(L"Microsoft.CmdPal.UI.exe"); + + if (processIds.size() == 0) + { + Logger::trace(L"Nothing To PROCESS_TERMINATE"); + return; + } + + for (DWORD pid : processIds) + { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + + if (hProcess != NULL) + { + TerminateProcess(hProcess, 0); + CloseHandle(hProcess); + } + } + } + +public: + CmdPal() + { + app_name = L"CmdPal"; + app_key = L"CmdPal"; + LoggerHelpers::init_logger(app_key, L"ModuleInterface", "CmdPal"); + } + + ~CmdPal() + { + if (m_enabled) + { + } + m_enabled = false; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + Logger::trace("CmdPal::destroy()"); + TerminateCmdPal(); + delete this; + } + + // Return the localized display name of the powertoy + virtual const wchar_t* get_name() override + { + return app_name.c_str(); + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + // Return the configured status for the gpo policy for the module + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredCmdPalEnabledValue(); + } + + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + virtual void call_custom_action(const wchar_t* /*action*/) override + { + } + + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + // If you don't need to do any custom processing of the settings, proceed + // to persists the values calling: + values.save_to_settings_file(); + // Otherwise call a custom function to process the settings before saving them to disk: + // save_settings(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + virtual void enable() + { + Logger::trace("CmdPal::enable()"); + + m_enabled = true; + + try + { + if (!package::GetRegisteredPackage(L"Microsoft.CommandPalette", false).has_value()) + { + Logger::info(L"CmdPal not installed. Installing..."); + + std::wstring installationFolder = get_module_folderpath(); +#if _DEBUG + std::wstring archSubdir = L"x64"; +#ifdef _M_ARM64 + archSubdir = L"ARM64"; +#endif + auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\", false); + auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\Dependencies\\" + archSubdir + L"\\", true); +#else + auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\", false); + auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\Dependencies\\", true); +#endif + + if (!msix.empty()) + { + auto msixPath = msix[0]; + + if (!package::RegisterPackage(msixPath, dependencies)) + { + Logger::error(L"Failed to install CmdPal package"); + } + } + } + } + catch (std::exception& e) + { + std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " }; + errorMessage += e.what(); + Logger::error(errorMessage); + } + + LaunchApp(); + } + + virtual void disable() + { + Logger::trace("CmdPal::disable()"); + TerminateCmdPal(); + + m_enabled = false; + } + + virtual bool on_hotkey(size_t) override + { + return false; + } + + virtual size_t get_hotkeys(Hotkey*, size_t) override + { + return 0; + } + + virtual bool is_enabled() override + { + return m_enabled; + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new CmdPal(); +} diff --git a/src/modules/cmdpal/CmdPalModuleInterface/packages.config b/src/modules/cmdpal/CmdPalModuleInterface/packages.config new file mode 100644 index 0000000000..09bfc449e2 --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/CmdPalModuleInterface/pch.cpp b/src/modules/cmdpal/CmdPalModuleInterface/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/cmdpal/CmdPalModuleInterface/pch.h b/src/modules/cmdpal/CmdPalModuleInterface/pch.h new file mode 100644 index 0000000000..30600d2e25 --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/pch.h @@ -0,0 +1,16 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +#include + +#include +#include +#include + +#endif //PCH_H diff --git a/src/modules/cmdpal/CmdPalModuleInterface/resource.h b/src/modules/cmdpal/CmdPalModuleInterface/resource.h new file mode 100644 index 0000000000..483f62d2bc --- /dev/null +++ b/src/modules/cmdpal/CmdPalModuleInterface/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by AlwaysOnTopModuleInterface.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys Command Palette Module" +#define INTERNAL_NAME "PowerToys.CmdPalModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.CmdPalModuleInterface.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Build.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Build.props new file mode 100644 index 0000000000..9eb15ca3cf --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Build.props @@ -0,0 +1,10 @@ + + + x64;ARM64 + true + Recommended + <_SkipUpgradeNetAnalyzersNuGetWarning>true + direct + $(Platform) + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props new file mode 100644 index 0000000000..4e4102de90 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props @@ -0,0 +1,16 @@ + + + true + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension.sln b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension.sln new file mode 100644 index 0000000000..47823e3856 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35507.96 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemplateCmdPalExtension", "TemplateCmdPalExtension\TemplateCmdPalExtension.csproj", "{79F86DE5-70B1-4EC1-9832-DF428B55E466}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.Build.0 = Debug|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.ActiveCfg = Debug|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.Build.0 = Debug|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.Deploy.0 = Debug|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.ActiveCfg = Debug|x86 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.Build.0 = Debug|x86 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.Deploy.0 = Debug|x86 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.ActiveCfg = Release|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.Build.0 = Release|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.Deploy.0 = Release|ARM64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.ActiveCfg = Release|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.Build.0 = Release|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.Deploy.0 = Release|x64 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.ActiveCfg = Release|x86 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.Build.0 = Release|x86 + {79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.Deploy.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CEDBC581-5818-4350-BC8A-A1ECE687D357} + EndGlobalSection +EndGlobal diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/LockScreenLogo.scale-200.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/SplashScreen.scale-200.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..32f486a867 Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square150x150Logo.scale-200.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..53ee3777ea Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.scale-200.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..f713bba67f Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..dc9f5bea0c Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/StoreLogo.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/StoreLogo.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..8b4a5d0dd5 Binary files /dev/null and b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Package.appxmanifest b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Package.appxmanifest new file mode 100644 index 0000000000..625ee4dfec --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Package.appxmanifest @@ -0,0 +1,80 @@ + + + + + + + + + TemplateDisplayName + A Lone Developer + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Pages/TemplateCmdPalExtensionPage.cs b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Pages/TemplateCmdPalExtensionPage.cs new file mode 100644 index 0000000000..93da2b191a --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Pages/TemplateCmdPalExtensionPage.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace TemplateCmdPalExtension; + +internal sealed partial class TemplateCmdPalExtensionPage : ListPage +{ + public TemplateCmdPalExtensionPage() + { + Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png"); + Title = "TemplateDisplayName"; + Name = "Open"; + } + + public override IListItem[] GetItems() + { + return [ + new ListItem(new NoOpCommand()) { Title = "TODO: Implement your extension here" } + ]; + } +} diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Program.cs b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Program.cs new file mode 100644 index 0000000000..7325c7c960 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace TemplateCmdPalExtension; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + using ExtensionServer server = new(); + var extensionDisposedEvent = new ManualResetEvent(false); + var extensionInstance = new TemplateCmdPalExtension(extensionDisposedEvent); + + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + server.RegisterExtension(() => extensionInstance); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as soon as it is disposed. + extensionDisposedEvent.WaitOne(); + } + else + { + Console.WriteLine("Not being launched as a Extension... exiting."); + } + } +} diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..79690066ff --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,15 @@ + + + + + FileSystem + ARM64 + win-arm64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + True + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..c53b5882ae --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,15 @@ + + + + + FileSystem + x64 + win-x64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + True + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/launchSettings.json b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..9e90cf7012 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "TemplateCmdPalExtension (Package)": { + "commandName": "MsixPackage", + "doNotLaunchApp": true + }, + "TemplateCmdPalExtension (Unpackaged)": { + "commandName": "Project" + } + } +} diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.cs b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.cs new file mode 100644 index 0000000000..ac6659e3b4 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace TemplateCmdPalExtension; + +[ComVisible(true)] +[Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")] +[ComDefaultInterface(typeof(IExtension))] +public sealed partial class TemplateCmdPalExtension : IExtension, IDisposable +{ + private readonly ManualResetEvent _extensionDisposedEvent; + + private readonly TemplateCmdPalExtensionCommandsProvider _provider = new(); + + public TemplateCmdPalExtension(ManualResetEvent extensionDisposedEvent) + { + this._extensionDisposedEvent = extensionDisposedEvent; + } + + public object? GetProvider(ProviderType providerType) + { + return providerType switch + { + ProviderType.Commands => _provider, + _ => null, + }; + } + + public void Dispose() => this._extensionDisposedEvent.Set(); +} diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj new file mode 100644 index 0000000000..6b58eb0e05 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtension.csproj @@ -0,0 +1,67 @@ + + + WinExe + TemplateCmdPalExtension + app.manifest + + 10.0.22621.57 + net9.0-windows10.0.22621.0 + 10.0.19041.0 + 10.0.19041.0 + win-x64;win-arm64 + + win-$(Platform).pubxml + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + true + true + 2 + + IL2081 + + true + true + + diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtensionCommandsProvider.cs b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtensionCommandsProvider.cs new file mode 100644 index 0000000000..45acf71856 --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/TemplateCmdPalExtensionCommandsProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace TemplateCmdPalExtension; + +public partial class TemplateCmdPalExtensionCommandsProvider : CommandProvider +{ + private readonly ICommandItem[] _commands; + + public TemplateCmdPalExtensionCommandsProvider() + { + DisplayName = "TemplateDisplayName"; + Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png"); + _commands = [ + new CommandItem(new TemplateCmdPalExtensionPage()) { Title = DisplayName }, + ]; + } + + public override ICommandItem[] TopLevelCommands() + { + return _commands; + } + +} diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/app.manifest b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/app.manifest new file mode 100644 index 0000000000..930a1b5cfa --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/TemplateCmdPalExtension/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/nuget.config b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/nuget.config new file mode 100644 index 0000000000..e6a17ffdfe --- /dev/null +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs new file mode 100644 index 0000000000..c90b45373f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps; + +public partial class AllAppsCommandProvider : CommandProvider +{ + public static readonly AllAppsPage Page = new(); + + private readonly CommandItem _listItem; + + public AllAppsCommandProvider() + { + Id = "AllApps"; + DisplayName = Resources.installed_apps; + Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg"); + Settings = AllAppsSettings.Instance.Settings; + + _listItem = new(Page) { Subtitle = Resources.search_installed_apps }; + } + + public override ICommandItem[] TopLevelCommands() => [_listItem]; + + public ICommandItem? LookupApp(string displayName) + { + var items = Page.GetItems(); + + // We're going to do this search in two directions: + // First, is this name a substring of any app... + var nameMatches = items.Where(i => i.Title.Contains(displayName)); + + // ... Then, does any app have this name as a substring ... + // Only get one of these - "Terminal Preview" contains both "Terminal" and "Terminal Preview", so just take the best one + var appMatches = items.Where(i => displayName.Contains(i.Title)).OrderByDescending(i => i.Title.Length).Take(1); + + // ... Now, combine those two + var both = nameMatches.Concat(appMatches); + + if (both.Count() == 1) + { + return both.First(); + } + else if (nameMatches.Count() == 1 && appMatches.Count() == 1) + { + if (nameMatches.First() == appMatches.First()) + { + return nameMatches.First(); + } + } + + return null; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs new file mode 100644 index 0000000000..ed724794fd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Apps.Programs; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps; + +public sealed partial class AllAppsPage : ListPage +{ + private readonly Lock _listLock = new(); + private AppListItem[] allAppsSection = []; + + public AllAppsPage() + { + this.Name = Resources.all_apps; + this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg"); + this.ShowDetails = true; + this.IsLoading = true; + this.PlaceholderText = Resources.search_installed_apps_placeholder; + + Task.Run(() => + { + lock (_listLock) + { + BuildListItems(); + } + }); + } + + public override IListItem[] GetItems() + { + if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload()) + { + lock (_listLock) + { + BuildListItems(); + } + } + + return allAppsSection; + } + + private void BuildListItems() + { + this.IsLoading = true; + + Stopwatch stopwatch = new(); + stopwatch.Start(); + + List apps = GetPrograms(); + + this.allAppsSection = apps + .Select((app) => new AppListItem(app, true)) + .ToArray(); + + this.IsLoading = false; + + AppCache.Instance.Value.ResetReloadFlag(); + + stopwatch.Stop(); + Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); + } + + internal List GetPrograms() + { + IEnumerable uwpResults = AppCache.Instance.Value.UWPs + .Where((application) => application.Enabled) + .Select(app => + new AppItem() + { + Name = app.Name, + Subtitle = app.Description, + Type = UWPApplication.Type(), + IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty, + DirPath = app.Location, + UserModelId = app.UserModelId, + IsPackaged = true, + Commands = app.GetCommands(), + }); + + IEnumerable win32Results = AppCache.Instance.Value.Win32s + .Where((application) => application.Enabled && application.Valid) + .Select(app => + { + string icoPath = string.IsNullOrEmpty(app.IcoPath) ? + (app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ? + app.IcoPath : + app.FullPath) : + app.IcoPath; + + // icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath; + icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? + app.FullPath : + icoPath; + return new AppItem() + { + Name = app.Name, + Subtitle = app.Description, + Type = app.Type(), + IcoPath = icoPath, + ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath, + DirPath = app.Location, + Commands = app.GetCommands(), + }; + }); + + return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppCache.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppCache.cs new file mode 100644 index 0000000000..cb80c5e8c5 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppCache.cs @@ -0,0 +1,90 @@ +// 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.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Programs; +using Microsoft.CmdPal.Ext.Apps.Storage; +using Microsoft.CmdPal.Ext.Apps.Utils; + +namespace Microsoft.CmdPal.Ext.Apps; + +public sealed class AppCache : IDisposable +{ + private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper; + + private PackageRepository _packageRepository; + + private Win32ProgramRepository _win32ProgramRepository; + + private bool _disposed; + + public IList Win32s => _win32ProgramRepository.Items; + + public IList UWPs => _packageRepository.Items; + + public static readonly Lazy Instance = new(() => new()); + + public AppCache() + { + _win32ProgramRepositoryHelper = new Win32ProgramFileSystemWatchers(); + _win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper.FileSystemWatchers.Cast().ToList(), AllAppsSettings.Instance, _win32ProgramRepositoryHelper.PathsToWatch); + + _packageRepository = new PackageRepository(new PackageCatalogWrapper()); + + var a = Task.Run(() => + { + _win32ProgramRepository.IndexPrograms(); + }); + + var b = Task.Run(() => + { + _packageRepository.IndexPrograms(); + UpdateUWPIconPath(ThemeHelper.GetCurrentTheme()); + }); + + Task.WaitAll(a, b); + + AllAppsSettings.Instance.LastIndexTime = DateTime.Today; + } + + private void UpdateUWPIconPath(Theme theme) + { + if (_packageRepository != null) + { + foreach (UWPApplication app in _packageRepository) + { + app.UpdateLogoPath(theme); + } + } + } + + public bool ShouldReload() => _packageRepository.ShouldReload() || _win32ProgramRepository.ShouldReload(); + + public void ResetReloadFlag() + { + _packageRepository.ResetReloadFlag(); + _win32ProgramRepository.ResetReloadFlag(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _win32ProgramRepositoryHelper?.Dispose(); + _disposed = true; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs new file mode 100644 index 0000000000..4bb26bed67 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Apps.Programs; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps; + +internal sealed class AppItem +{ + public string Name { get; set; } = string.Empty; + + public string Subtitle { get; set; } = string.Empty; + + public string Type { get; set; } = string.Empty; + + public string IcoPath { get; set; } = string.Empty; + + public string ExePath { get; set; } = string.Empty; + + public string DirPath { get; set; } = string.Empty; + + public string UserModelId { get; set; } = string.Empty; + + public bool IsPackaged { get; set; } + + public List? Commands { get; set; } + + public AppItem() + { + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs new file mode 100644 index 0000000000..1ab8df76ef --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +internal sealed partial class AppListItem : ListItem +{ + private readonly AppItem _app; + private static readonly Tag _appTag = new("App"); + + private readonly Lazy
_details; + private readonly Lazy _icon; + + public override IDetails? Details { get => _details.Value; set => base.Details = value; } + + public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; } + + public AppListItem(AppItem app, bool useThumbnails) + : base(new AppCommand(app)) + { + _app = app; + Title = app.Name; + Subtitle = app.Subtitle; + Tags = [_appTag]; + MoreCommands = _app.Commands!.ToArray(); + + _details = new Lazy
(() => BuildDetails()); + _icon = new Lazy(() => + { + var t = FetchIcon(useThumbnails); + t.Wait(); + return t.Result; + }); + } + + private Details BuildDetails() + { + var metadata = new List(); + metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } }); + if (!_app.IsPackaged) + { + metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } }); + } + + return new Details() + { + Title = this.Title, + HeroImage = this.Icon ?? new IconInfo(string.Empty), + Metadata = metadata.ToArray(), + }; + } + + public async Task FetchIcon(bool useThumbnails) + { + IconInfo? icon = null; + if (_app.IsPackaged) + { + icon = new IconInfo(_app.IcoPath); + if (_details.IsValueCreated) + { + _details.Value.HeroImage = icon; + } + + return icon; + } + + if (useThumbnails) + { + try + { + var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath); + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + icon = new IconInfo(data, data); + } + } + catch + { + } + + icon = icon ?? new IconInfo(_app.IcoPath); + } + else + { + icon = new IconInfo(_app.IcoPath); + } + + if (_details.IsValueCreated) + { + _details.Value.HeroImage = icon; + } + + return icon; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png new file mode 100644 index 0000000000..34ca984532 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg new file mode 100644 index 0000000000..3fcd53bc89 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj new file mode 100644 index 0000000000..24782335ed --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj @@ -0,0 +1,52 @@ + + + + Microsoft.CmdPal.Ext.Apps + enable + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt new file mode 100644 index 0000000000..c0c94348c5 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt @@ -0,0 +1,19 @@ +GetPhysicallyInstalledSystemMemory +GlobalMemoryStatusEx +GetSystemInfo +CoCreateInstance +SetForegroundWindow +IsIconic +RegisterHotKey +SetWindowLongPtr +CallWindowProc +ShowWindow +SetForegroundWindow +SetFocus +SetActiveWindow +MonitorFromWindow +GetMonitorInfo +SHCreateStreamOnFileEx +CoAllowSetForegroundWindow +SHCreateStreamOnFileEx +SHLoadIndirectString \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs new file mode 100644 index 0000000000..420128ef29 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application +[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781")] +[ComImport] +public class AppxFactory +{ +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs new file mode 100644 index 0000000000..83a9fbb146 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Windows.Win32.System.Com; +using static Microsoft.CmdPal.Ext.Apps.Utils.Native; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +public static class AppxPackageHelper +{ + private static readonly IAppxFactory AppxFactory = (IAppxFactory)new AppxFactory(); + + // This function returns a list of attributes of applications + internal static IEnumerable GetAppsFromManifest(IStream stream) + { + var reader = AppxFactory.CreateManifestReader(stream); + var manifestApps = reader.GetApplications(); + + while (manifestApps.GetHasCurrent()) + { + var manifestApp = manifestApps.GetCurrent(); + var hr = manifestApp.GetStringValue("AppListEntry", out var appListEntry); + _ = CheckHRAndReturnOrThrow(hr, appListEntry); + if (appListEntry != "none") + { + yield return manifestApp; + } + + manifestApps.MoveNext(); + } + } + + internal static T CheckHRAndReturnOrThrow(HRESULT hr, T result) + { + if (hr != HRESULT.S_OK) + { + Marshal.ThrowExceptionForHR((int)hr); + } + + return result; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs new file mode 100644 index 0000000000..32fb3f2890 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs +[Flags] +public enum ActivateOptions +{ + None = 0x00000000, + DesignMode = 0x00000001, + NoErrorUI = 0x00000002, + NoSplashScreen = 0x00000004, +} + +// ApplicationActivationManager +[ComImport] +[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IApplicationActivationManager +{ + IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); +} + +// Application Activation Manager Class +[ComImport] +[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] +public class ApplicationActivationManager : IApplicationActivationManager +{ + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/] + public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs new file mode 100644 index 0000000000..7af82b74ab --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Windows.Win32.System.Com; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IAppxFactory +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")] + void _VtblGap0_2(); // skip 2 methods + + internal IAppxManifestReader CreateManifestReader(IStream inputStream); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs new file mode 100644 index 0000000000..1ca12d3c29 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using static Microsoft.CmdPal.Ext.Apps.Utils.Native; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IAppxManifestApplication +{ + [PreserveSig] + HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); + + [PreserveSig] + HRESULT GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs new file mode 100644 index 0000000000..f7152a0813 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IAppxManifestApplicationsEnumerator +{ + IAppxManifestApplication GetCurrent(); + + bool GetHasCurrent(); + + bool MoveNext(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs new file mode 100644 index 0000000000..4c61e6f069 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using static Microsoft.CmdPal.Ext.Apps.Utils.Native; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IAppxManifestProperties +{ + [PreserveSig] + HRESULT GetBoolValue([MarshalAs(UnmanagedType.LPWStr)] string name, out bool value); + + [PreserveSig] + HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs new file mode 100644 index 0000000000..20c7fb62f6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Guid("4E1BD148-55A0-4480-A3D1-15544710637C")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IAppxManifestReader +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")] + void _VtblGap0_1(); // skip 1 method + + IAppxManifestProperties GetProperties(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")] + void _VtblGap1_5(); // skip 5 methods + + IAppxManifestApplicationsEnumerator GetApplications(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs new file mode 100644 index 0000000000..84021d5970 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs @@ -0,0 +1,20 @@ +// 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.Apps.Programs; + +public interface IPackage +{ + string Name { get; } + + string FullName { get; } + + string FamilyName { get; } + + bool IsFramework { get; } + + bool IsDevelopmentMode { get; } + + string InstalledLocation { get; } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs new file mode 100644 index 0000000000..d7a3383a32 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +public interface IPackageManager +{ + IEnumerable FindPackagesForCurrentUser(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs new file mode 100644 index 0000000000..2350cf6ae6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs @@ -0,0 +1,18 @@ +// 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.Apps.Programs; + +public interface IProgram +{ + string UniqueIdentifier { get; set; } + + string Name { get; } + + string Description { get; set; } + + string Location { get; } + + bool Enabled { get; set; } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs new file mode 100644 index 0000000000..2de4ef92cc --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +public enum LogoType +{ + Error, + Colored, + HighContrast, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs new file mode 100644 index 0000000000..be70a0ba95 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Windows.Management.Deployment; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +public class PackageManagerWrapper : IPackageManager +{ + private readonly PackageManager _packageManager; + + public PackageManagerWrapper() + { + _packageManager = new PackageManager(); + } + + public IEnumerable FindPackagesForCurrentUser() + { + var user = WindowsIdentity.GetCurrent().User; + + if (user != null) + { + var pkgs = _packageManager.FindPackagesForUser(user.Value); + + return pkgs.Select(PackageWrapper.GetWrapperFromPackage).Where(package => package != null); + } + + return Enumerable.Empty(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs new file mode 100644 index 0000000000..2de128c05c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Windows.Foundation.Metadata; +using Package = Windows.ApplicationModel.Package; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +public class PackageWrapper : IPackage +{ + public string Name { get; } = string.Empty; + + public string FullName { get; } = string.Empty; + + public string FamilyName { get; } = string.Empty; + + public bool IsFramework { get; } + + public bool IsDevelopmentMode { get; } + + public string InstalledLocation { get; } = string.Empty; + + public PackageWrapper() + { + } + + public PackageWrapper(string name, string fullName, string familyName, bool isFramework, bool isDevelopmentMode, string installedLocation) + { + Name = name; + FullName = fullName; + FamilyName = familyName; + IsFramework = isFramework; + IsDevelopmentMode = isDevelopmentMode; + InstalledLocation = installedLocation; + } + + private static readonly Lazy IsPackageDotInstallationPathAvailable = new(() => + ApiInformation.IsPropertyPresent(typeof(Package).FullName, nameof(Package.InstalledLocation.Path))); + + public static PackageWrapper GetWrapperFromPackage(Package package) + { + ArgumentNullException.ThrowIfNull(package); + + string path; + try + { + path = IsPackageDotInstallationPathAvailable.Value ? GetInstalledPath(package) : package.InstalledLocation.Path; + } + catch (Exception e) when (e is ArgumentException || e is FileNotFoundException || e is DirectoryNotFoundException) + { + return new PackageWrapper( + package.Id.Name, + package.Id.FullName, + package.Id.FamilyName, + package.IsFramework, + package.IsDevelopmentMode, + string.Empty); + } + + return new PackageWrapper( + package.Id.Name, + package.Id.FullName, + package.Id.FamilyName, + package.IsFramework, + package.IsDevelopmentMode, + path); + } + + // This is a separate method so the reference to .InstalledPath won't be loaded in API versions which do not support this API (e.g. older then Build 19041) + private static string GetInstalledPath(Package package) + => package.InstalledLocation.Path; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs new file mode 100644 index 0000000000..d22b42772e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs @@ -0,0 +1,24 @@ +// 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.Apps.Programs; + +/// +/// Contains user added folder location contents as well as all user disabled applications +/// +/// +/// Win32 class applications set UniqueIdentifier using their full file path +/// UWP class applications set UniqueIdentifier using their Application User Model ID +/// Custom user added program sources set UniqueIdentifier using their location +/// +public class ProgramSource +{ + public string Location { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public bool Enabled { get; set; } = true; + + public string UniqueIdentifier { get; set; } = string.Empty; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs new file mode 100644 index 0000000000..beba2b185a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs @@ -0,0 +1,203 @@ +// 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.Abstractions; +using System.Linq; +using System.Xml.Linq; +using Microsoft.CmdPal.Ext.Apps.Utils; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using static Microsoft.CmdPal.Ext.Apps.Utils.Native; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Serializable] +public partial class UWP +{ + private static readonly IPath Path = new FileSystem().Path; + + private static readonly Dictionary _versionFromNamespace = new() + { + { "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 }, + { "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 }, + { "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 }, + }; + + public string Name { get; } + + public string FullName { get; } + + public string FamilyName { get; } + + public string Location { get; set; } = string.Empty; + + // Localized path based on windows display language + public string LocationLocalized { get; set; } = string.Empty; + + public IList Apps { get; private set; } = new List(); + + public PackageVersion Version { get; set; } + + public static IPackageManager PackageManagerWrapper { get; set; } = new PackageManagerWrapper(); + + public UWP(IPackage package) + { + ArgumentNullException.ThrowIfNull(package); + + Name = package.Name; + FullName = package.FullName; + FamilyName = package.FamilyName; + } + + public void InitializeAppInfo(string installedLocation) + { + Location = installedLocation; + LocationLocalized = ShellLocalization.Instance.GetLocalizedPath(installedLocation); + var path = Path.Combine(installedLocation, "AppxManifest.xml"); + + var namespaces = XmlNamespaces(path); + InitPackageVersion(namespaces); + + const uint noAttribute = 0x80; + + var access = (uint)STGM.READ; + var hResult = PInvoke.SHCreateStreamOnFileEx(path, access, noAttribute, false, null, out IStream stream); + + // S_OK + if (hResult == 0) + { + Apps = AppxPackageHelper.GetAppsFromManifest(stream).Select(appInManifest => new UWPApplication(appInManifest, this)).Where(a => + { + var valid = + !string.IsNullOrEmpty(a.UserModelId) && + !string.IsNullOrEmpty(a.DisplayName) && + a.AppListEntry != "none"; + + return valid; + }).ToList(); + } + else + { + Apps = Array.Empty(); + } + } + + // http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx + private static string[] XmlNamespaces(string path) + { + var z = XDocument.Load(path); + if (z.Root != null) + { + var namespaces = z.Root.Attributes(). + Where(a => a.IsNamespaceDeclaration). + GroupBy( + a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName, + a => XNamespace.Get(a.Value)).Select( + g => g.First().ToString()).ToArray(); + return namespaces; + } + else + { + return Array.Empty(); + } + } + + private void InitPackageVersion(string[] namespaces) + { + foreach (var n in _versionFromNamespace.Keys.Where(namespaces.Contains)) + { + Version = _versionFromNamespace[n]; + return; + } + + Version = PackageVersion.Unknown; + } + + public static UWPApplication[] All() + { + var windows10 = new Version(10, 0); + var support = Environment.OSVersion.Version.Major >= windows10.Major; + if (support) + { + var applications = CurrentUserPackages().AsParallel().SelectMany(p => + { + UWP u; + try + { + u = new UWP(p); + u.InitializeAppInfo(p.InstalledLocation); + } + catch (Exception ) + { + return Array.Empty(); + } + + return u.Apps; + }); + + var updatedListWithoutDisabledApps = applications + .Where(t1 => AllAppsSettings.Instance.DisabledProgramSources.All(x => x.UniqueIdentifier != t1.UniqueIdentifier)) + .Select(x => x); + + return updatedListWithoutDisabledApps.ToArray(); + } + else + { + return Array.Empty(); + } + } + + private static IEnumerable CurrentUserPackages() + { + return PackageManagerWrapper.FindPackagesForCurrentUser().Where(p => + { + try + { + var f = p.IsFramework; + var path = p.InstalledLocation; + return !f && !string.IsNullOrEmpty(path); + } + catch (Exception ) + { + return false; + } + }); + } + + public override string ToString() + { + return FamilyName; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "Using CurrentCultureIgnoreCase since this is used with FamilyName")] + public override bool Equals(object? obj) + { + if (obj is UWP uwp) + { + // Using CurrentCultureIgnoreCase since this is used with FamilyName + return FamilyName.Equals(uwp.FamilyName, StringComparison.CurrentCultureIgnoreCase); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + // Using CurrentCultureIgnoreCase since this is used with FamilyName + return FamilyName.GetHashCode(StringComparison.CurrentCultureIgnoreCase); + } + + public enum PackageVersion + { + Windows10, + Windows81, + Windows8, + Unknown, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs new file mode 100644 index 0000000000..6a4cf94480 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -0,0 +1,608 @@ +// 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.Abstractions; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml; +using Microsoft.CmdPal.Ext.Apps.Commands; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.Utils; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using static Microsoft.CmdPal.Ext.Apps.Utils.Native; +using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Serializable] +public class UWPApplication : IProgram +{ + private static readonly IFileSystem FileSystem = new FileSystem(); + private static readonly IPath Path = FileSystem.Path; + private static readonly IFile File = FileSystem.File; + + public string AppListEntry { get; set; } = string.Empty; + + public string UniqueIdentifier { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } + + public string UserModelId { get; set; } + + public string BackgroundColor { get; set; } + + public string EntryPoint { get; set; } + + public string Name => DisplayName; + + public string Location => Package.Location; + + // Localized path based on windows display language + public string LocationLocalized => Package.LocationLocalized; + + public bool Enabled { get; set; } + + public bool CanRunElevated { get; set; } + + public string LogoPath { get; set; } = string.Empty; + + public LogoType LogoType { get; set; } + + public UWP Package { get; set; } + + private string logoUri; + + private const string ContrastWhite = "contrast-white"; + + private const string ContrastBlack = "contrast-black"; + + // Function to set the subtitle based on the Type of application + public static string Type() + { + return Resources.packaged_application; + } + + public List GetCommands() + { + List commands = new List(); + + if (CanRunElevated) + { + commands.Add( + new CommandContextItem( + new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))); + + // We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users. + } + + commands.Add( + new CommandContextItem( + new OpenPathCommand(Location) + { + Name = Resources.open_containing_folder, + Icon = new("\ue838"), + })); + + commands.Add( + new CommandContextItem( + new OpenInConsoleCommand(Package.Location))); + + return commands; + } + + public UWPApplication(IAppxManifestApplication manifestApp, UWP package) + { + ArgumentNullException.ThrowIfNull(manifestApp); + + var hr = manifestApp.GetAppUserModelId(out var tmpUserModelId); + UserModelId = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUserModelId); + + hr = manifestApp.GetAppUserModelId(out var tmpUniqueIdentifier); + UniqueIdentifier = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUniqueIdentifier); + + hr = manifestApp.GetStringValue("DisplayName", out var tmpDisplayName); + DisplayName = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDisplayName); + + hr = manifestApp.GetStringValue("Description", out var tmpDescription); + Description = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDescription); + + hr = manifestApp.GetStringValue("BackgroundColor", out var tmpBackgroundColor); + BackgroundColor = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpBackgroundColor); + + hr = manifestApp.GetStringValue("EntryPoint", out var tmpEntryPoint); + EntryPoint = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpEntryPoint); + + Package = package ?? throw new ArgumentNullException(nameof(package)); + + DisplayName = ResourceFromPri(package.FullName, DisplayName); + Description = ResourceFromPri(package.FullName, Description); + logoUri = LogoUriFromManifest(manifestApp); + + Enabled = true; + CanRunElevated = IfApplicationCanRunElevated(); + } + + private bool IfApplicationCanRunElevated() + { + if (EntryPoint == "Windows.FullTrustApplication") + { + return true; + } + else + { + var manifest = Package.Location + "\\AppxManifest.xml"; + if (File.Exists(manifest)) + { + try + { + // Check the manifest to verify if the Trust Level for the application is "mediumIL" + var file = File.ReadAllText(manifest); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(file); + var xmlRoot = xmlDoc.DocumentElement; + var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); + namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10"); + var trustLevelNode = xmlRoot?.SelectSingleNode("//*[local-name()='Application' and @uap10:TrustLevel]", namespaceManager); // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes + + if (trustLevelNode?.Attributes?["uap10:TrustLevel"]?.Value == "mediumIL") + { + return true; + } + } + catch (Exception) + { + } + } + } + + return false; + } + + internal string ResourceFromPri(string packageFullName, string resourceReference) + { + const string prefix = "ms-resource:"; + + // Using OrdinalIgnoreCase since this is used internally + if (!string.IsNullOrWhiteSpace(resourceReference) && resourceReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + // magic comes from @talynone + // https://github.com/talynone/Wox.Plugin.WindowsUniversalAppLauncher/blob/master/StoreAppLauncher/Helpers/NativeApiHelper.cs#L139-L153 + var key = resourceReference.Substring(prefix.Length); + string parsed; + var parsedFallback = string.Empty; + + // Using Ordinal/OrdinalIgnoreCase since these are used internally + if (key.StartsWith("//", StringComparison.Ordinal)) + { + parsed = prefix + key; + } + else if (key.StartsWith('/')) + { + parsed = prefix + "//" + key; + } + else if (key.Contains("resources", StringComparison.OrdinalIgnoreCase)) + { + parsed = prefix + key; + } + else + { + parsed = prefix + "///resources/" + key; + + // e.g. for Windows Terminal version >= 1.12 DisplayName and Description resources are not in the 'resources' subtree + parsedFallback = prefix + "///" + key; + } + + var outBuffer = new StringBuilder(128); + var source = $"@{{{packageFullName}? {parsed}}}"; + var capacity = (uint)outBuffer.Capacity; + var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero); + if (hResult != HRESULT.S_OK) + { + if (!string.IsNullOrEmpty(parsedFallback)) + { + var sourceFallback = $"@{{{packageFullName}? {parsedFallback}}}"; + hResult = SHLoadIndirectString(sourceFallback, outBuffer, capacity, IntPtr.Zero); + if (hResult == HRESULT.S_OK) + { + var loaded = outBuffer.ToString(); + if (!string.IsNullOrEmpty(loaded)) + { + return loaded; + } + else + { + return string.Empty; + } + } + } + + // https://github.com/Wox-launcher/Wox/issues/964 + // known hresult 2147942522: + // 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'. + // for + // Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description + // Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription + return string.Empty; + } + else + { + var loaded = outBuffer.ToString(); + if (!string.IsNullOrEmpty(loaded)) + { + return loaded; + } + else + { + return string.Empty; + } + } + } + else + { + return resourceReference; + } + } + + private static readonly Dictionary _logoKeyFromVersion = new Dictionary + { + { PackageVersion.Windows10, "Square44x44Logo" }, + { PackageVersion.Windows81, "Square30x30Logo" }, + { PackageVersion.Windows8, "SmallLogo" }, + }; + + internal string LogoUriFromManifest(IAppxManifestApplication app) + { + if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key)) + { + var hr = app.GetStringValue(key, out var logoUriFromApp); + _ = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, logoUriFromApp); + return logoUriFromApp; + } + else + { + return string.Empty; + } + } + + public void UpdateLogoPath(Theme theme) + { + LogoPathFromUri(logoUri, theme); + } + + // scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables, + private static readonly Dictionary> _scaleFactors = new Dictionary> + { + { PackageVersion.Windows10, new List { 100, 125, 150, 200, 400 } }, + { PackageVersion.Windows81, new List { 100, 120, 140, 160, 180 } }, + { PackageVersion.Windows8, new List { 100 } }, + }; + + private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false) + { + var extension = Path.GetExtension(path); + if (extension != null) + { + var end = path.Length - extension.Length; + var prefix = path.Substring(0, end); + var paths = new List { }; + + if (!highContrast) + { + paths.Add(path); + } + + if (_scaleFactors.TryGetValue(Package.Version, out var factors)) + { + foreach (var factor in factors) + { + if (highContrast) + { + paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}"); + paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}"); + } + else + { + paths.Add($"{prefix}.scale-{factor}{extension}"); + } + } + } + + var selectedIconPath = paths.FirstOrDefault(File.Exists); + if (!string.IsNullOrEmpty(selectedIconPath)) + { + LogoPath = selectedIconPath; + if (highContrast) + { + LogoType = LogoType.HighContrast; + } + else + { + LogoType = LogoType.Colored; + } + + return true; + } + } + + return false; + } + + private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false) + { + var extension = Path.GetExtension(path); + if (extension != null) + { + var end = path.Length - extension.Length; + var prefix = path.Substring(0, end); + var paths = new List { }; + const int appIconSize = 36; + var targetSizes = new List { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 }.AsParallel(); + var pathFactorPairs = new Dictionary(); + + foreach (var factor in targetSizes) + { + if (highContrast) + { + var suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}"; + var prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}"; + paths.Add(suffixThemePath); + paths.Add(prefixThemePath); + pathFactorPairs.Add(suffixThemePath, factor); + pathFactorPairs.Add(prefixThemePath, factor); + } + else + { + var simplePath = $"{prefix}.targetsize-{factor}{extension}"; + var altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}"; + paths.Add(simplePath); + paths.Add(altformUnPlatedPath); + pathFactorPairs.Add(simplePath, factor); + pathFactorPairs.Add(altformUnPlatedPath, factor); + } + } + + var selectedIconPath = paths.OrderBy(x => Math.Abs(pathFactorPairs.GetValueOrDefault(x) - appIconSize)).FirstOrDefault(File.Exists); + if (!string.IsNullOrEmpty(selectedIconPath)) + { + LogoPath = selectedIconPath; + if (highContrast) + { + LogoType = LogoType.HighContrast; + } + else + { + LogoType = LogoType.Colored; + } + + return true; + } + } + + return false; + } + + private bool SetColoredIcon(string path, string colorscheme) + { + var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme); + if (isSetColoredScaleIcon) + { + return true; + } + + var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme); + if (isSetColoredTargetIcon) + { + return true; + } + + var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true); + if (isSetHighContrastScaleIcon) + { + return true; + } + + var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true); + if (isSetHighContrastTargetIcon) + { + return true; + } + + return false; + } + + private bool SetHighContrastIcon(string path, string colorscheme) + { + var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true); + if (isSetHighContrastScaleIcon) + { + return true; + } + + var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true); + if (isSetHighContrastTargetIcon) + { + return true; + } + + var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme); + if (isSetColoredScaleIcon) + { + return true; + } + + var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme); + if (isSetColoredTargetIcon) + { + return true; + } + + return false; + } + + internal void LogoPathFromUri(string uri, Theme theme) + { + // all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets + // windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx + // windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size + // windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx + string path; + bool isLogoUriSet; + + // Using Ordinal since this is used internally with uri + if (uri.Contains('\\', StringComparison.Ordinal)) + { + path = Path.Combine(Package.Location, uri); + } + else + { + // for C:\Windows\MiracastView etc + path = Path.Combine(Package.Location, "Assets", uri); + } + + switch (theme) + { + case Theme.HighContrastBlack: + case Theme.HighContrastOne: + case Theme.HighContrastTwo: + isLogoUriSet = SetHighContrastIcon(path, ContrastBlack); + break; + case Theme.HighContrastWhite: + isLogoUriSet = SetHighContrastIcon(path, ContrastWhite); + break; + case Theme.Light: + isLogoUriSet = SetColoredIcon(path, ContrastWhite); + break; + default: + isLogoUriSet = SetColoredIcon(path, ContrastBlack); + break; + } + + if (!isLogoUriSet) + { + LogoPath = string.Empty; + LogoType = LogoType.Error; + } + } + + /* + public ImageSource Logo() + { + if (LogoType == LogoType.Colored) + { + var logo = ImageFromPath(LogoPath); + var platedImage = PlatedImage(logo); + return platedImage; + } + else + { + return ImageFromPath(LogoPath); + } + } + + private const int _dpiScale100 = 96; + + private ImageSource PlatedImage(BitmapImage image) + { + if (!string.IsNullOrEmpty(BackgroundColor)) + { + string currentBackgroundColor; + if (BackgroundColor == "transparent") + { + // Using InvariantCulture since this is internal + currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture); + } + else + { + currentBackgroundColor = BackgroundColor; + } + + var padding = 8; + var width = image.Width + (2 * padding); + var height = image.Height + (2 * padding); + var x = 0; + var y = 0; + + var group = new DrawingGroup(); + var converted = ColorConverter.ConvertFromString(currentBackgroundColor); + if (converted != null) + { + var color = (Color)converted; + var brush = new SolidColorBrush(color); + var pen = new Pen(brush, 1); + var backgroundArea = new Rect(0, 0, width, height); + var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8); + var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry); + group.Children.Add(rectDrawing); + + var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height); + var imageDrawing = new ImageDrawing(image, imageArea); + group.Children.Add(imageDrawing); + + // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush + var visual = new DrawingVisual(); + var context = visual.RenderOpen(); + context.DrawDrawing(group); + context.Close(); + + var bitmap = new RenderTargetBitmap( + Convert.ToInt32(width), + Convert.ToInt32(height), + _dpiScale100, + _dpiScale100, + PixelFormats.Pbgra32); + + bitmap.Render(visual); + + return bitmap; + } + else + { + ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location); + + return new BitmapImage(new Uri(Constant.ErrorIcon)); + } + } + else + { + // todo use windows theme as background + return image; + } + } + + private BitmapImage ImageFromPath(string path) + { + if (File.Exists(path)) + { + var memoryStream = new MemoryStream(); + using (var fileStream = File.OpenRead(path)) + { + fileStream.CopyTo(memoryStream); + memoryStream.Position = 0; + + var image = new BitmapImage(); + image.BeginInit(); + image.StreamSource = memoryStream; + image.EndInit(); + return image; + } + } + else + { + // ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path); + return new BitmapImage(new Uri(ImageLoader.ErrorIconPath)); + } + } + */ + + public override string ToString() + { + return $"{DisplayName}: {Description}"; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs new file mode 100644 index 0000000000..48dfaa2f7e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs @@ -0,0 +1,847 @@ +// 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.ComponentModel.Design; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; +using System.Security; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows.Input; +using Microsoft.CmdPal.Ext.Apps.Commands; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.Utils; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +[Serializable] +public class Win32Program : IProgram +{ + public static readonly Win32Program InvalidProgram = new Win32Program { Valid = false, Enabled = false }; + + private static readonly IFileSystem FileSystem = new FileSystem(); + private static readonly IPath Path = FileSystem.Path; + private static readonly IFile File = FileSystem.File; + private static readonly IDirectory Directory = FileSystem.Directory; + + public string Name { get; set; } = string.Empty; + + // Localized name based on windows display language + public string NameLocalized { get; set; } = string.Empty; + + public string UniqueIdentifier { get; set; } = string.Empty; + + public string IcoPath { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + // Path of app executable or lnk target executable + public string FullPath { get; set; } = string.Empty; + + // Localized path based on windows display language + public string FullPathLocalized { get; set; } = string.Empty; + + public string ParentDirectory { get; set; } = string.Empty; + + public string ExecutableName { get; set; } = string.Empty; + + // Localized executable name based on windows display language + public string ExecutableNameLocalized { get; set; } = string.Empty; + + // Path to the lnk file on LnkProgram + public string LnkFilePath { get; set; } = string.Empty; + + public string LnkResolvedExecutableName { get; set; } = string.Empty; + + // Localized path based on windows display language + public string LnkResolvedExecutableNameLocalized { get; set; } = string.Empty; + + public bool Valid { get; set; } + + public bool Enabled { get; set; } + + public bool HasArguments => !string.IsNullOrEmpty(Arguments); + + public string Arguments { get; set; } = string.Empty; + + public string Location => ParentDirectory; + + public ApplicationType AppType { get; set; } + + // Wrappers for File Operations + public static IFileVersionInfoWrapper FileVersionInfoWrapper { get; set; } = new FileVersionInfoWrapper(); + + public static IFile FileWrapper { get; set; } = new FileSystem().File; + + private const string ShortcutExtension = "lnk"; + private const string ApplicationReferenceExtension = "appref-ms"; + private const string InternetShortcutExtension = "url"; + private static readonly HashSet ExecutableApplicationExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "cpl", "msc", "msi", "cmd", "ps1", "job", "msp", "mst", "sct", "ws", "wsh", "wsf" }; + + private const string ProxyWebApp = "_proxy.exe"; + private const string AppIdArgument = "--app-id"; + + public enum ApplicationType + { + WebApplication = 0, + InternetShortcutApplication = 1, + Win32Application = 2, + ShortcutApplication = 3, + ApprefApplication = 4, + RunCommand = 5, + Folder = 6, + GenericFile = 7, + } + + public bool IsWebApplication() + { + // To Filter PWAs when the user searches for the main application + // All Chromium based applications contain the --app-id argument + // Reference : https://codereview.chromium.org/399045 + // Using Ordinal IgnoreCase since this is used internally + return !string.IsNullOrEmpty(FullPath) && + !string.IsNullOrEmpty(Arguments) && + FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) && + Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase); + } + + // Condition to Filter pinned Web Applications or PWAs when searching for the main application + public bool FilterWebApplication(string query) + { + // If the app is not a web application, then do not filter it + if (!IsWebApplication()) + { + return false; + } + + var subqueries = query?.Split() ?? Array.Empty(); + var nameContainsQuery = false; + var pathContainsQuery = false; + + // check if any space separated query is a part of the app name or path name + foreach (var subquery in subqueries) + { + // Using OrdinalIgnoreCase since these are used internally + if (FullPath.Contains(subquery, StringComparison.OrdinalIgnoreCase)) + { + pathContainsQuery = true; + } + + if (Name.Contains(subquery, StringComparison.OrdinalIgnoreCase)) + { + nameContainsQuery = true; + } + } + + return pathContainsQuery && !nameContainsQuery; + } + + // Function to set the subtitle based on the Type of application + public string Type() + { + switch (AppType) + { + case ApplicationType.Win32Application: + case ApplicationType.ShortcutApplication: + case ApplicationType.ApprefApplication: + return Resources.application; + case ApplicationType.InternetShortcutApplication: + return Resources.internet_shortcut_application; + case ApplicationType.WebApplication: + return Resources.web_application; + case ApplicationType.RunCommand: + return Resources.run_command; + case ApplicationType.Folder: + return Resources.folder; + case ApplicationType.GenericFile: + return Resources.file; + default: + return string.Empty; + } + } + + public bool QueryEqualsNameForRunCommands(string query) + { + if (query != null && AppType == ApplicationType.RunCommand) + { + // Using OrdinalIgnoreCase since this is used internally + if (!query.Equals(Name, StringComparison.OrdinalIgnoreCase) && !query.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + + public List GetCommands() + { + List commands = new List(); + + if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile) + { + commands.Add(new CommandContextItem( + new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))); + + commands.Add(new CommandContextItem( + new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))); + } + + commands.Add(new CommandContextItem( + new OpenPathCommand(ParentDirectory))); + + commands.Add(new CommandContextItem( + new OpenInConsoleCommand(ParentDirectory))); + + return commands; + } + + public override string ToString() + { + return ExecutableName; + } + + private static Win32Program CreateWin32Program(string path) + { + try + { + var parentDir = Directory.GetParent(path); + + return new Win32Program + { + Name = Path.GetFileNameWithoutExtension(path), + ExecutableName = Path.GetFileName(path), + IcoPath = path, + + // Using InvariantCulture since this is user facing + FullPath = path, + UniqueIdentifier = path, + ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName, + Description = string.Empty, + Valid = true, + Enabled = true, + AppType = ApplicationType.Win32Application, + + // Localized name, path and executable based on windows display language + NameLocalized = ShellLocalization.Instance.GetLocalizedName(path), + FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(path), + ExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(path)), + }; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + return InvalidProgram; + } + catch (Exception) + { + return InvalidProgram; + } + } + + private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run|open)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled); + + // This function filters Internet Shortcut programs + private static Win32Program InternetShortcutProgram(string path) + { + try + { + // We don't want to read the whole file if we don't need to + var lines = FileWrapper.ReadLines(path); + var iconPath = string.Empty; + var urlPath = string.Empty; + var validApp = false; + + const string urlPrefix = "URL="; + const string iconFilePrefix = "IconFile="; + + foreach (var line in lines) + { + // Using OrdinalIgnoreCase since this is used internally + if (line.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase)) + { + urlPath = line.Substring(urlPrefix.Length); + + if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out var _)) + { + return InvalidProgram; + } + + // To filter out only those steam shortcuts which have 'run' or 'rungameid' as the hostname + if (InternetShortcutURLPrefixes.Match(urlPath).Success) + { + validApp = true; + } + } + else if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase)) + { + iconPath = line.Substring(iconFilePrefix.Length); + } + + // If we resolved an urlPath & and an iconPath quit reading the file + if (!string.IsNullOrEmpty(urlPath) && !string.IsNullOrEmpty(iconPath)) + { + break; + } + } + + if (!validApp) + { + return InvalidProgram; + } + + try + { + var parentDir = Directory.GetParent(path); + + return new Win32Program + { + Name = Path.GetFileNameWithoutExtension(path), + ExecutableName = Path.GetFileName(path), + IcoPath = iconPath, + FullPath = urlPath, + UniqueIdentifier = path, + ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName, + Valid = true, + Enabled = true, + AppType = ApplicationType.InternetShortcutApplication, + }; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + return InvalidProgram; + } + } + catch (Exception) + { + return InvalidProgram; + } + } + + private static Win32Program LnkProgram(string path) + { + try + { + var program = CreateWin32Program(path); + var shellLinkHelper = new ShellLinkHelper(); + var target = shellLinkHelper.RetrieveTargetPath(path); + + if (!string.IsNullOrEmpty(target)) + { + if (!(File.Exists(target) || Directory.Exists(target))) + { + // If the link points nowhere, consider it invalid. + return InvalidProgram; + } + + program.LnkFilePath = program.FullPath; + program.LnkResolvedExecutableName = Path.GetFileName(target); + program.LnkResolvedExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(target)); + + // Using CurrentCulture since this is user facing + program.FullPath = Path.GetFullPath(target); + program.FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(target); + + program.Arguments = shellLinkHelper.Arguments; + + // A .lnk could be a (Chrome) PWA, set correct AppType + program.AppType = program.IsWebApplication() + ? ApplicationType.WebApplication + : GetAppTypeFromPath(target); + + var description = shellLinkHelper.Description; + if (!string.IsNullOrEmpty(description)) + { + program.Description = description; + } + else + { + var info = FileVersionInfoWrapper.GetVersionInfo(target); + if (!string.IsNullOrEmpty(info?.FileDescription)) + { + program.Description = info.FileDescription; + } + } + } + + return program; + } + catch (System.IO.FileLoadException) + { + return InvalidProgram; + } + + // Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in. + // Error caused likely due to trying to get the description of the program + catch (Exception) + { + return InvalidProgram; + } + } + + private static Win32Program ExeProgram(string path) + { + try + { + var program = CreateWin32Program(path); + var info = FileVersionInfoWrapper.GetVersionInfo(path); + if (!string.IsNullOrEmpty(info?.FileDescription)) + { + program.Description = info.FileDescription; + } + + return program; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + return InvalidProgram; + } + catch (FileNotFoundException) + { + return InvalidProgram; + } + catch (Exception) + { + return InvalidProgram; + } + } + + // Function to get the application type, given the path to the application + public static ApplicationType GetAppTypeFromPath(string path) + { + ArgumentNullException.ThrowIfNull(path); + + var extension = Extension(path); + + // Using OrdinalIgnoreCase since these are used internally with paths + if (ExecutableApplicationExtensions.Contains(extension)) + { + return ApplicationType.Win32Application; + } + else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase)) + { + return ApplicationType.ShortcutApplication; + } + else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase)) + { + return ApplicationType.ApprefApplication; + } + else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase)) + { + return ApplicationType.InternetShortcutApplication; + } + else if (string.IsNullOrEmpty(extension) && System.IO.Directory.Exists(path)) + { + return ApplicationType.Folder; + } + + return ApplicationType.GenericFile; + } + + // Function to get the Win32 application, given the path to the application + public static Win32Program? GetAppFromPath(string path) + { + ArgumentNullException.ThrowIfNull(path); + + Win32Program? app; + switch (GetAppTypeFromPath(path)) + { + case ApplicationType.Win32Application: + app = ExeProgram(path); + break; + case ApplicationType.ShortcutApplication: + app = LnkProgram(path); + break; + case ApplicationType.ApprefApplication: + app = CreateWin32Program(path); + app.AppType = ApplicationType.ApprefApplication; + break; + case ApplicationType.InternetShortcutApplication: + app = InternetShortcutProgram(path); + break; + case ApplicationType.WebApplication: + case ApplicationType.RunCommand: + case ApplicationType.Folder: + case ApplicationType.GenericFile: + default: + app = null; + break; + } + + // if the app is valid, only then return the application, else return null + return app?.Valid == true + ? app + : null; + } + + private static IEnumerable ProgramPaths(string directory, IList suffixes, bool recursiveSearch = true) + { + if (!Directory.Exists(directory)) + { + return Array.Empty(); + } + + var files = new List(); + var folderQueue = new Queue(); + folderQueue.Enqueue(directory); + + // Keep track of already visited directories to avoid cycles. + var alreadyVisited = new HashSet(); + + do + { + var currentDirectory = folderQueue.Dequeue(); + + if (alreadyVisited.Contains(currentDirectory)) + { + continue; + } + + alreadyVisited.Add(currentDirectory); + + try + { + foreach (var suffix in suffixes) + { + try + { + files.AddRange(Directory.EnumerateFiles(currentDirectory, $"*.{suffix}", SearchOption.TopDirectoryOnly)); + } + catch (DirectoryNotFoundException) + { + } + } + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + } + catch (Exception) + { + } + + try + { + // If the search is set to be non-recursive, then do not enqueue the child directories. + if (!recursiveSearch) + { + continue; + } + + foreach (var childDirectory in Directory.EnumerateDirectories(currentDirectory, "*", new EnumerationOptions() + { + // https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions?view=net-6.0 + // Exclude directories with the Reparse Point file attribute, to avoid loops due to symbolic links / directory junction / mount points. + AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint, + RecurseSubdirectories = false, + })) + { + folderQueue.Enqueue(childDirectory); + } + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + } + catch (Exception) + { + } + } + while (folderQueue.Count > 0); + + return files; + } + + private static string Extension(string path) + { + // Using InvariantCulture since this is user facing + var extension = Path.GetExtension(path)?.ToLowerInvariant(); + + return !string.IsNullOrEmpty(extension) + ? extension.Substring(1) + : string.Empty; + } + + private static IEnumerable CustomProgramPaths(IEnumerable sources, IList suffixes) + => sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled) + .SelectMany(programSource => ProgramPaths(programSource.Location, suffixes)) + .ToList() ?? Enumerable.Empty(); + + // Function to obtain the list of applications, the locations of which have been added to the env variable PATH + private static List PathEnvironmentProgramPaths(IList suffixes) + { + // To get all the locations stored in the PATH env variable + var pathEnvVariable = Environment.GetEnvironmentVariable("PATH"); + var searchPaths = pathEnvVariable?.Split(Path.PathSeparator); + var toFilterAllPaths = new List(); + var isRecursiveSearch = true; + + if (searchPaths is not null) + { + foreach (var path in searchPaths) + { + if (path.Length > 0) + { + // to expand any environment variables present in the path + var directory = Environment.ExpandEnvironmentVariables(path); + var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch); + toFilterAllPaths.AddRange(paths); + } + } + } + + return toFilterAllPaths; + } + + private static List IndexPath(IList suffixes, List indexLocations) + => indexLocations + .SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes)) + .ToList(); + + private static List StartMenuProgramPaths(IList suffixes) + { + var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu); + var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu); + var indexLocation = new List() { directory1, directory2 }; + + return IndexPath(suffixes, indexLocation); + } + + private static List DesktopProgramPaths(IList suffixes) + { + var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory); + + var indexLocation = new List() { directory1, directory2 }; + + return IndexPath(suffixes, indexLocation); + } + + private static List RegistryAppProgramPaths(IList suffixes) + { + // https://msdn.microsoft.com/library/windows/desktop/ee872121 + const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; + var paths = new List(); + using (var root = Registry.LocalMachine.OpenSubKey(appPaths)) + { + if (root != null) + { + paths.AddRange(GetPathsFromRegistry(root)); + } + } + + using (var root = Registry.CurrentUser.OpenSubKey(appPaths)) + { + if (root != null) + { + paths.AddRange(GetPathsFromRegistry(root)); + } + } + + return paths + .Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase))) + .Select(ExpandEnvironmentVariables) + .Where(path => path is not null) + .ToList(); + } + + private static IEnumerable GetPathsFromRegistry(RegistryKey root) + => root + .GetSubKeyNames() + .Select(x => GetPathFromRegistrySubkey(root, x)); + + private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey) + { + var path = string.Empty; + try + { + using (var key = root.OpenSubKey(subkey)) + { + if (key == null) + { + return string.Empty; + } + + var defaultValue = string.Empty; + path = key.GetValue(defaultValue) as string; + } + + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + // fix path like this: ""\"C:\\folder\\executable.exe\"" + return path = path.Trim('"', ' '); + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + return string.Empty; + } + } + + private static string ExpandEnvironmentVariables(string path) => + !string.IsNullOrEmpty(path) + ? Environment.ExpandEnvironmentVariables(path) + : string.Empty; + + // Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage + public override int GetHashCode() + => Win32ProgramEqualityComparer.Default.GetHashCode(this); + + public override bool Equals(object? obj) + => obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program); + + private sealed class Win32ProgramEqualityComparer : IEqualityComparer + { + public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer(); + + public bool Equals(Win32Program? app1, Win32Program? app2) + { + if (app1 == null && app2 == null) + { + return true; + } + + return app1 != null + && app2 != null + && (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant()) + .Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant())); + } + + public int GetHashCode(Win32Program obj) + => (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode(); + } + + public static List DeduplicatePrograms(IEnumerable programs) + => new HashSet(programs, Win32ProgramEqualityComparer.Default).ToList(); + + private static Win32Program GetProgramFromPath(string path) + { + var extension = Extension(path); + if (ExecutableApplicationExtensions.Contains(extension)) + { + return ExeProgram(path); + } + + switch (extension) + { + case ShortcutExtension: + return LnkProgram(path); + case ApplicationReferenceExtension: + return CreateWin32Program(path); + case InternetShortcutExtension: + return InternetShortcutProgram(path); + default: + return InvalidProgram; + } + } + + private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string? icoPath) + { + icoPath = null; + + if (program.AppType != ApplicationType.RunCommand) + { + return false; + } + + if (string.IsNullOrEmpty(program.FullPath)) + { + return false; + } + + // https://msdn.microsoft.com/library/windows/desktop/ee872121 + try + { + var redirectionPath = ReparsePoint.GetTarget(program.FullPath); + if (string.IsNullOrEmpty(redirectionPath)) + { + return false; + } + + icoPath = ExpandEnvironmentVariables(redirectionPath); + return true; + } + catch (IOException) + { + } + + icoPath = null; + return false; + } + + private static Win32Program GetRunCommandProgramFromPath(string path) + { + var program = GetProgramFromPath(path); + if (program.Valid) + { + program.AppType = ApplicationType.RunCommand; + + if (TryGetIcoPathForRunCommandProgram(program, out var icoPath)) + { + program.IcoPath = icoPath ?? string.Empty; + } + } + + return program; + } + + public static IList All(AllAppsSettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + + try + { + // Set an initial size to an expected size to prevent multiple hashSet resizes + const int defaultHashsetSize = 1000; + + // Multiple paths could have the same programPaths and we don't want to resolve / lookup them multiple times + var paths = new HashSet(defaultHashsetSize); + var runCommandPaths = new HashSet(defaultHashsetSize); + + // Parallelize multiple sources, and priority based on paths which most likely contain .lnks which are formatted + var sources = new (bool IsEnabled, Func> GetPaths)[] + { + (true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)), + (settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)), + (settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)), + (settings.EnableRegistrySource, () => RegistryAppProgramPaths(settings.ProgramSuffixes)), + }; + + // Run commands are always set as AppType "RunCommand" + var runCommandSources = new (bool IsEnabled, Func> GetPaths)[] + { + (settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.RunCommandSuffixes)), + }; + + var disabledProgramsList = settings.DisabledProgramSources; + + // Get all paths but exclude all normal .Executables + paths.UnionWith(sources + .AsParallel() + .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty()) + .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)) + .Where(path => !ExecutableApplicationExtensions.Contains(Extension(path)))); + runCommandPaths.UnionWith(runCommandSources + .AsParallel() + .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty()) + .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))); + + var programs = paths.AsParallel().Select(source => GetProgramFromPath(source)); + var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source)); + + return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true)); + } + catch (Exception) + { + return Array.Empty(); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs new file mode 100644 index 0000000000..9b3f54a21f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.CmdPal.Ext.Bookmarks.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +internal sealed partial class AddBookmarkForm : FormContent +{ + internal event TypedEventHandler? AddedCommand; + + private readonly BookmarkData? _bookmark; + + public AddBookmarkForm(BookmarkData? bookmark) + { + _bookmark = bookmark; + 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)}}, + "isRequired": true, + "errorMessage": "{{Resources.bookmarks_form_name_required}}" + }, + { + "type": "Input.Text", + "style": "text", + "id": "bookmark", + "value": {{JsonSerializer.Serialize(url)}}, + "label": "{{Resources.bookmarks_form_bookmark_label}}", + "isRequired": true, + "errorMessage": "{{Resources.bookmarks_form_bookmark_required}}" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "{{Resources.bookmarks_form_save}}", + "data": { + "name": "name", + "bookmark": "bookmark" + } + } + ] +} +"""; + } + + public override CommandResult SubmitForm(string payload) + { + var formInput = JsonNode.Parse(payload); + if (formInput == null) + { + return CommandResult.GoHome(); + } + + // get the name and url out of the values + var formName = formInput["name"] ?? string.Empty; + var formBookmark = formInput["bookmark"] ?? string.Empty; + var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}'); + + // Determine the type of the bookmark + string bookmarkType; + + if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + bookmarkType = "web"; + } + else if (File.Exists(formBookmark.ToString())) + { + bookmarkType = "file"; + } + else if (Directory.Exists(formBookmark.ToString())) + { + bookmarkType = "folder"; + } + else + { + // Default to web if we can't determine the type + bookmarkType = "web"; + } + + var updated = _bookmark ?? new BookmarkData(); + updated.Name = formName.ToString(); + updated.Bookmark = formBookmark.ToString(); + updated.Type = bookmarkType; + + AddedCommand?.Invoke(this, updated); + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs new file mode 100644 index 0000000000..dcc03c83ab --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs @@ -0,0 +1,34 @@ +// 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.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +internal sealed partial class AddBookmarkPage : ContentPage +{ + private readonly AddBookmarkForm _addBookmark; + + internal event TypedEventHandler? AddedCommand + { + add => _addBookmark.AddedCommand += value; + remove => _addBookmark.AddedCommand -= value; + } + + public override IContent[] GetContent() => [_addBookmark]; + + public AddBookmarkPage(BookmarkData? bookmark) + { + var name = bookmark?.Name ?? string.Empty; + var url = bookmark?.Bookmark ?? string.Empty; + Icon = new IconInfo("\ued0e"); + 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; + _addBookmark = new(bookmark); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs new file mode 100644 index 0000000000..af6f1ef245 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +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('}'); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs new file mode 100644 index 0000000000..f5a5745e1b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Microsoft.CmdPal.Ext.Bookmarks.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.System; + +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 _placeholderNames; + + private readonly string _bookmark = string.Empty; + + // TODO pass in an array of placeholders + public BookmarkPlaceholderForm(string name, string url, string type) + { + _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 == null) + { + return CommandResult.GoHome(); + } + + foreach (var (key, value) in formObject) + { + var placeholderString = $"{{{key}}}"; + var placeholderData = value?.ToString(); + target = target.Replace(placeholderString, placeholderData); + } + + try + { + var uri = UrlCommand.GetUri(target); + if (uri != null) + { + _ = Launcher.LaunchUriAsync(uri); + } + else + { + // throw new UriFormatException("The provided URL is not valid."); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}"); + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs new file mode 100644 index 0000000000..d30f72bd95 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +internal sealed partial class BookmarkPlaceholderPage : ContentPage +{ + private readonly FormContent _bookmarkPlaceholder; + + public override IContent[] GetContent() => [_bookmarkPlaceholder]; + + public BookmarkPlaceholderPage(BookmarkData data) + : this(data.Name, data.Bookmark, data.Type) + { + } + + public BookmarkPlaceholderPage(string name, string url, string type) + { + Name = name; + Icon = new IconInfo(UrlCommand.IconFromUrl(url, type)); + _bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs new file mode 100644 index 0000000000..7c3a1dd1e0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public sealed class Bookmarks +{ + public List Data { get; set; } = []; + + private static readonly JsonSerializerOptions _jsonOptions = new() + { + IncludeFields = true, + }; + + public static Bookmarks ReadFromFile(string path) + { + var data = new Bookmarks(); + + // if the file exists, load it and append the new item + if (File.Exists(path)) + { + var jsonStringReading = File.ReadAllText(path); + + if (!string.IsNullOrEmpty(jsonStringReading)) + { + data = JsonSerializer.Deserialize(jsonStringReading, _jsonOptions) ?? new Bookmarks(); + } + } + + return data; + } + + public static void WriteToFile(string path, Bookmarks data) + { + var jsonString = JsonSerializer.Serialize(data, _jsonOptions); + + File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs new file mode 100644 index 0000000000..1ecf748a70 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +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 partial class BookmarksCommandProvider : CommandProvider +{ + private readonly List _commands = []; + + private readonly AddBookmarkPage _addNewCommand = new(null); + + private Bookmarks? _bookmarks; + + public static IconInfo DeleteIcon { get; private set; } = new("\uE74D"); // Delete + + public static IconInfo EditIcon { get; private set; } = new("\uE70F"); // Edit + + public BookmarksCommandProvider() + { + Id = "Bookmarks"; + DisplayName = Resources.bookmarks_display_name; + Icon = new IconInfo("\uE718"); // Pin + + _addNewCommand.AddedCommand += AddNewCommand_AddedCommand; + } + + private void AddNewCommand_AddedCommand(object sender, BookmarkData args) + { + ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})"); + if (_bookmarks != null) + { + _bookmarks.Data.Add(args); + } + + SaveAndUpdateCommands(); + } + + // In the edit path, `args` was already in _bookmarks, we just updated it + private void Edit_AddedCommand(object sender, BookmarkData args) + { + ExtensionHost.LogMessage($"Edited bookmark ({args.Name},{args.Bookmark})"); + + SaveAndUpdateCommands(); + } + + private void SaveAndUpdateCommands() + { + if (_bookmarks != null) + { + var jsonPath = BookmarksCommandProvider.StateJsonPath(); + Bookmarks.WriteToFile(jsonPath, _bookmarks); + } + + LoadCommands(); + RaiseItemsChanged(0); + } + + private void LoadCommands() + { + List collected = []; + collected.Add(new CommandItem(_addNewCommand)); + + if (_bookmarks == null) + { + LoadBookmarksFromFile(); + } + + if (_bookmarks != null) + { + collected.AddRange(_bookmarks.Data.Select(BookmarkToCommandItem)); + } + + _commands.Clear(); + _commands.AddRange(collected); + } + + private void LoadBookmarksFromFile() + { + try + { + var jsonFile = StateJsonPath(); + if (File.Exists(jsonFile)) + { + _bookmarks = Bookmarks.ReadFromFile(jsonFile); + } + } + catch (Exception ex) + { + // debug log error + Debug.WriteLine($"Error loading commands: {ex.Message}"); + } + + if (_bookmarks == 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 contextMenu = []; + + // Add commands for folder types + if (command is UrlCommand urlCommand) + { + if (urlCommand.Type == "folder") + { + contextMenu.Add( + new CommandContextItem(new DirectoryPage(urlCommand.Url))); + + contextMenu.Add( + new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url))); + } + + listItem.Subtitle = urlCommand.Url; + } + + var edit = new AddBookmarkPage(bookmark) { Icon = 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 != null) + { + ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})"); + + _bookmarks.Data.Remove(bookmark); + + SaveAndUpdateCommands(); + } + }, + result: CommandResult.KeepOpen()) + { + IsCritical = true, + Icon = DeleteIcon, + }; + contextMenu.Add(delete); + + listItem.MoreCommands = contextMenu.ToArray(); + + return listItem; + } + + public override ICommandItem[] TopLevelCommands() + { + if (_commands.Count == 0) + { + LoadCommands(); + } + + return _commands.ToArray(); + } + + internal static string StateJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return System.IO.Path.Combine(directory, "bookmarks.json"); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj new file mode 100644 index 0000000000..dd7f12d7b8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj @@ -0,0 +1,31 @@ + + + + Microsoft.CmdPal.Ext.Bookmarks + enable + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.Bookmarks.pri + + + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png new file mode 100644 index 0000000000..819d18aada Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg new file mode 100644 index 0000000000..a9375b0526 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs new file mode 100644 index 0000000000..c4f9b1d58f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Text; +using Microsoft.CmdPal.Ext.Calc.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Calc; + +public partial class CalculatorCommandProvider : CommandProvider +{ + private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle }; + private readonly FallbackCalculatorItem _fallback = new(); + + public CalculatorCommandProvider() + { + Id = "Calculator"; + DisplayName = Resources.calculator_display_name; + Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg"); + } + + public override ICommandItem[] TopLevelCommands() => [_listItem]; + + public override IFallbackCommandItem[] FallbackCommands() => [_fallback]; +} + +// The calculator page is a dynamic list page +// * The first command is where we display the results. Title=result, Subtitle=query +// - The default command is `SaveCommand`. +// - When you save, insert into list at spot 1 +// - change SearchText to the result +// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard +// * The rest of the items are previously saved results +// - Command is a CopyCommand +// - Each item also sets the TextToSuggest to the result +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] +public sealed partial class CalculatorListPage : DynamicListPage +{ + private readonly List _items = []; + private readonly SaveCommand _saveCommand = new(); + private readonly CopyTextCommand _copyContextCommand; + private readonly CommandContextItem _copyContextMenuItem; + private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error); + + public CalculatorListPage() + { + Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg"); + Name = Resources.calculator_title; + PlaceholderText = Resources.calculator_placeholder_text; + Id = "com.microsoft.cmdpal.calculator"; + + _copyContextCommand = new CopyTextCommand(string.Empty); + _copyContextMenuItem = new CommandContextItem(_copyContextCommand); + + _items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") }); + + UpdateSearchText(string.Empty, string.Empty); + + _saveCommand.SaveRequested += HandleSave; + } + + private void HandleSave(object sender, object args) + { + var lastResult = _items[0].Title; + if (!string.IsNullOrEmpty(lastResult)) + { + var li = new ListItem(new CopyTextCommand(lastResult)) + { + Title = _items[0].Title, + Subtitle = _items[0].Subtitle, + TextToSuggest = lastResult, + }; + _items.Insert(1, li); + _items[0].Subtitle = string.Empty; + SearchText = lastResult; + this.RaiseItemsChanged(this._items.Count); + } + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + var firstItem = _items[0]; + if (string.IsNullOrEmpty(newSearch)) + { + firstItem.Title = Resources.calculator_placeholder_text; + firstItem.Subtitle = string.Empty; + firstItem.MoreCommands = []; + } + else + { + _copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty; + firstItem.Title = result; + firstItem.Subtitle = newSearch; + firstItem.MoreCommands = [_copyContextMenuItem]; + } + } + + internal static bool ParseQuery(string equation, out string result) + { + try + { + var resultNumber = new DataTable().Compute(equation, null); + result = resultNumber.ToString() ?? string.Empty; + return true; + } + catch (Exception e) + { + result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message); + return false; + } + } + + public override IListItem[] GetItems() => _items.ToArray(); +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] +public sealed partial class SaveCommand : InvokableCommand +{ + public event TypedEventHandler SaveRequested; + + public SaveCommand() + { + Name = Resources.calculator_save_command_name; + } + + public override ICommandResult Invoke() + { + SaveRequested?.Invoke(this, this); + return CommandResult.KeepOpen(); + } +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] +internal sealed partial class FallbackCalculatorItem : FallbackCommandItem +{ + private readonly CopyTextCommand _copyCommand = new(string.Empty); + + public FallbackCalculatorItem() + : base(new NoOpCommand(), Resources.calculator_title) + { + Command = _copyCommand; + _copyCommand.Name = string.Empty; + Title = string.Empty; + Subtitle = Resources.calculator_placeholder_text; + Icon = new IconInfo("\ue8ef"); // Calculator + } + + public override void UpdateQuery(string query) + { + if (CalculatorListPage.ParseQuery(query, out var result)) + { + _copyCommand.Text = result; + _copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name; + Title = result; + + // we have to make the subtitle the equation, + // so that we will still string match the original query + // Otherwise, something like 1+2 will have a title of "3" and not match + Subtitle = query; + } + else + { + _copyCommand.Text = string.Empty; + _copyCommand.Name = string.Empty; + Title = string.Empty; + Subtitle = string.Empty; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj new file mode 100644 index 0000000000..68a38c1ca2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj @@ -0,0 +1,41 @@ + + + + Microsoft.CmdPal.Ext.Calc + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.Calc.pri + + + + + + + + Resources.resx + True + True + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs new file mode 100644 index 0000000000..85fa7e90d1 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.ClipboardHistory.Pages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory; + +public partial class ClipboardHistoryCommandsProvider : CommandProvider +{ + private readonly ListItem _clipboardHistoryListItem; + + public ClipboardHistoryCommandsProvider() + { + _clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage()) + { + Title = "Search Clipboard History", + Icon = new IconInfo("\xE8C8"), // Copy icon + }; + + DisplayName = $"Clipboard History"; + Icon = new IconInfo("\xE8C8"); // Copy icon + Id = "Windows.ClipboardHistory"; + } + + public override IListItem[] TopLevelCommands() + { + return [_clipboardHistoryListItem]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs new file mode 100644 index 0000000000..de019f1699 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands; + +internal sealed partial class CopyCommand : InvokableCommand +{ + private readonly ClipboardItem _clipboardItem; + private readonly ClipboardFormat _clipboardFormat; + + internal CopyCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + _clipboardItem = clipboardItem; + _clipboardFormat = clipboardFormat; + Name = "Copy"; + if (clipboardFormat == ClipboardFormat.Text) + { + Icon = new("\xE8C8"); // Copy icon + } + else + { + Icon = new("\xE8B9"); // Picture icon + } + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat); + return CommandResult.ShowToast("Copied to clipboard"); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs new file mode 100644 index 0000000000..f104fd424e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs @@ -0,0 +1,44 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Common.Messages; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CommandPalette.Extensions.Toolkit; + +using Windows.ApplicationModel.DataTransfer; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands; + +internal sealed partial class PasteCommand : InvokableCommand +{ + private readonly ClipboardItem _clipboardItem; + private readonly ClipboardFormat _clipboardFormat; + + internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + _clipboardItem = clipboardItem; + _clipboardFormat = clipboardFormat; + Name = "Paste"; + Icon = new("\xE8C8"); // Copy icon + } + + private void HideWindow() + { + // TODO GH #524: This isn't great - this requires us to have Secret Sauce in + // the clipboard extension to be able to manipulate the HWND. + // We probably need to put some window manipulation into the API, but + // what form that takes is not clear yet. + WeakReferenceMessenger.Default.Send(new()); + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat); + HideWindow(); + ClipboardHelper.SendPasteKeyCombination(); + Clipboard.DeleteItemFromHistory(_clipboardItem.Item); + return CommandResult.ShowToast("Pasting"); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs new file mode 100644 index 0000000000..bbfec3c491 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs @@ -0,0 +1,262 @@ +// 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.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.Data.Html; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.Streams; +using Windows.System; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory; + +internal static class ClipboardHelper +{ + private static readonly HashSet ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" }; + + private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats = + [ + (StandardDataFormats.Text, ClipboardFormat.Text), + (StandardDataFormats.Html, ClipboardFormat.Html), + (StandardDataFormats.Bitmap, ClipboardFormat.Image), + ]; + + internal static async Task GetAvailableClipboardFormatsAsync(DataPackageView clipboardData) + { + var availableClipboardFormats = DataFormats.Aggregate( + ClipboardFormat.None, + (result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result); + + if (clipboardData.Contains(StandardDataFormats.StorageItems)) + { + var storageItems = await clipboardData.GetStorageItemsAsync(); + + if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType)) + { + availableClipboardFormats |= ClipboardFormat.ImageFile; + } + } + + return availableClipboardFormats; + } + + internal static void SetClipboardTextContent(string text) + { + if (!string.IsNullOrEmpty(text)) + { + DataPackage output = new(); + output.SetText(text); + try + { + // Clipboard.SetContentWithOptions(output, null); + Clipboard.SetContent(output); + Flush(); + ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" }); + } + catch (COMException ex) + { + ExtensionHost.LogMessage($"Error: {ex.HResult}\n{ex.Source}\n{ex.StackTrace}"); + } + } + } + + private static bool Flush() + { + // TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. + // Calling inside a loop makes it work. + // Exception is: The operation is not permitted because the calling application is not the owner of the data on the clipboard. + const int maxAttempts = 5; + for (var i = 1; i <= maxAttempts; i++) + { + try + { + Task.Run(Clipboard.Flush).Wait(); + return true; + } + catch (Exception ex) + { + if (i == maxAttempts) + { + ExtensionHost.LogMessage(new LogMessage() + { + Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}", + }); + } + } + } + + return false; + } + + private static async Task FlushAsync() => await Task.Run(Flush); + + internal static async Task SetClipboardFileContentAsync(string fileName) + { + var storageFile = await StorageFile.GetFileFromPathAsync(fileName); + + DataPackage output = new(); + output.SetStorageItems([storageFile]); + Clipboard.SetContent(output); + + await FlushAsync(); + } + + internal static void SetClipboardImageContent(RandomAccessStreamReference image) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Copied image to clipboard" }); + + if (image is not null) + { + DataPackage output = new(); + output.SetBitmap(image); + Clipboard.SetContentWithOptions(output, null); + + Flush(); + } + } + + internal static void SetClipboardContent(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + switch (clipboardFormat) + { + case ClipboardFormat.Text: + if (clipboardItem.Content == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); + return; + } + else + { + SetClipboardTextContent(clipboardItem.Content); + } + + break; + + case ClipboardFormat.Image: + if (clipboardItem.ImageData == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); + return; + } + else + { + SetClipboardImageContent(clipboardItem.ImageData); + } + + break; + + default: + ExtensionHost.LogMessage(new LogMessage { Message = "Unsupported clipboard format." }); + break; + } + } + + // Function to send a single key event + private static void SendSingleKeyboardInput(short keyCode, uint keyStatus) + { + var ignoreKeyEventFlag = (UIntPtr)0x5555; + + var inputShift = new NativeMethods.INPUT + { + type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD, + data = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = keyCode, + dwFlags = keyStatus, + + // Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead. + dwExtraInfo = ignoreKeyEventFlag, + }, + }, + }; + + var inputs = new NativeMethods.INPUT[] { inputShift }; + _ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size); + } + + internal static void SendPasteKeyCombination() + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Sending paste keys..." }); + + SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp); + + // Send Ctrl + V + SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown); + SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown); + SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp); + SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp); + + ExtensionHost.LogMessage(new LogMessage() { Message = "Paste sent" }); + } + + internal static async Task GetClipboardTextOrHtmlTextAsync(DataPackageView clipboardData) + { + if (clipboardData.Contains(StandardDataFormats.Text)) + { + return await clipboardData.GetTextAsync(); + } + else if (clipboardData.Contains(StandardDataFormats.Html)) + { + var html = await clipboardData.GetHtmlFormatAsync(); + return HtmlUtilities.ConvertToText(html); + } + else + { + return string.Empty; + } + } + + internal static async Task GetClipboardHtmlContentAsync(DataPackageView clipboardData) => + clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty; + + internal static async Task GetClipboardImageContentAsync(DataPackageView clipboardData) + { + using var stream = await GetClipboardImageStreamAsync(clipboardData); + if (stream != null) + { + var decoder = await BitmapDecoder.CreateAsync(stream); + return await decoder.GetSoftwareBitmapAsync(); + } + + return null; + } + + private static async Task GetClipboardImageStreamAsync(DataPackageView clipboardData) + { + if (clipboardData.Contains(StandardDataFormats.StorageItems)) + { + var storageItems = await clipboardData.GetStorageItemsAsync(); + var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null; + if (file != null) + { + return await file.OpenReadAsync(); + } + } + + if (clipboardData.Contains(StandardDataFormats.Bitmap)) + { + var bitmap = await clipboardData.GetBitmapAsync(); + return await bitmap.OpenReadAsync(); + } + + return null; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs new file mode 100644 index 0000000000..50ff346103 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; + +internal static class NativeMethods +{ + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct INPUT + { + internal INPUTTYPE type; + internal InputUnion data; + + internal static int Size => Marshal.SizeOf(typeof(INPUT)); + } + + [StructLayout(LayoutKind.Explicit)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct InputUnion + { + [FieldOffset(0)] + internal MOUSEINPUT mi; + [FieldOffset(0)] + internal KEYBDINPUT ki; + [FieldOffset(0)] + internal HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct MOUSEINPUT + { + internal int dx; + internal int dy; + internal int mouseData; + internal uint dwFlags; + internal uint time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct KEYBDINPUT + { + internal short wVk; + internal short wScan; + internal uint dwFlags; + internal int time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct HARDWAREINPUT + { + internal int uMsg; + internal short wParamL; + internal short wParamH; + } + + internal enum INPUTTYPE : uint + { + INPUT_MOUSE = 0, + INPUT_KEYBOARD = 1, + INPUT_HARDWARE = 2, + } + + [Flags] + internal enum KeyEventF + { + KeyDown = 0x0000, + ExtendedKey = 0x0001, + KeyUp = 0x0002, + Unicode = 0x0004, + Scancode = 0x0008, + } + + [DllImport("user32.dll")] + internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + internal static extern short GetAsyncKeyState(int vKey); + + [StructLayout(LayoutKind.Sequential)] + internal struct PointInter + { + public int X; + public int Y; + + public static explicit operator Point(PointInter point) => new(point.X, point.Y); + } + + [DllImport("user32.dll")] + internal static extern bool GetCursorPos(out PointInter lpPoint); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj new file mode 100644 index 0000000000..1d583e279b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj @@ -0,0 +1,17 @@ + + + + Microsoft.CmdPal.Ext.ClipboardHistory + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + enable + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs new file mode 100644 index 0000000000..7ba791c930 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models; + +[Flags] +public enum ClipboardFormat +{ + None, + Text = 1 << 0, + Html = 1 << 1, + Audio = 1 << 2, + Image = 1 << 3, + ImageFile = 1 << 4, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs new file mode 100644 index 0000000000..94c5a86dc3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.CmdPal.Ext.ClipboardHistory.Commands; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models; + +public class ClipboardItem +{ + public string? Content { get; set; } + + public required ClipboardHistoryItem Item { get; set; } + + public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue; + + public RandomAccessStreamReference? ImageData { get; set; } + + public string GetDataType() + { + // Check if there is valid image data + if (IsImage) + { + return "Image"; + } + + // Check if there is valid text content + return IsText ? "Text" : "Unknown"; + } + + [MemberNotNullWhen(true, nameof(ImageData))] + private bool IsImage => ImageData != null; + + [MemberNotNullWhen(true, nameof(Content))] + private bool IsText => !string.IsNullOrEmpty(Content); + + public static List ShiftLinesLeft(List lines) + { + // Determine the minimum leading whitespace + var minLeadingWhitespace = lines + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Min(line => line.TakeWhile(char.IsWhiteSpace).Count()); + + // Check if all lines have at least that much leading whitespace + if (lines.Any(line => line.TakeWhile(char.IsWhiteSpace).Count() < minLeadingWhitespace)) + { + return lines; // Return the original lines if any line doesn't have enough leading whitespace + } + + // Remove the minimum leading whitespace from each line + var shiftedLines = lines.Select(line => line.Substring(minLeadingWhitespace)).ToList(); + + return shiftedLines; + } + + public static List StripLeadingWhitespace(List lines) + { + // Determine the minimum leading whitespace + var minLeadingWhitespace = lines + .Min(line => line.TakeWhile(char.IsWhiteSpace).Count()); + + // Remove the minimum leading whitespace from each line + var shiftedLines = lines.Select(line => + line.Length >= minLeadingWhitespace + ? line.Substring(minLeadingWhitespace) + : line).ToList(); + + return shiftedLines; + } + + public ListItem ToListItem() + { + ListItem listItem; + + List metadata = []; + metadata.Add(new DetailsElement() + { + Key = "Copied on", + Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)), + }); + + if (IsImage) + { + var iconData = new IconData(ImageData); + var heroImage = new IconInfo(iconData, iconData); + listItem = new(new CopyCommand(this, ClipboardFormat.Image)) + { + // Placeholder subtitle as there’s no BitmapImage dimensions to retrieve + Title = "Image Data", + Details = new Details() + { + HeroImage = heroImage, + Title = GetDataType(), + Body = Timestamp.ToString(CultureInfo.InvariantCulture), + Metadata = metadata.ToArray(), + }, + MoreCommands = [ + new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image)) + ], + }; + } + else if (IsText) + { + var splitContent = Content.Split("\n"); + var head = splitContent.AsSpan(0, Math.Min(3, splitContent.Length)).ToArray().ToList(); + var preview2 = string.Join( + "\n", + StripLeadingWhitespace(head)); + + listItem = new(new CopyCommand(this, ClipboardFormat.Text)) + { + Title = preview2, + + Details = new Details + { + Title = GetDataType(), + Body = $"```text\n{Content}\n```", + Metadata = metadata.ToArray(), + }, + MoreCommands = [ + new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)), + ], + }; + } + else + { + listItem = new(new NoOpCommand()) + { + Title = "Unknown", + Subtitle = GetDataType(), + Details = new Details { Title = GetDataType() }, + }; + } + + return listItem; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs new file mode 100644 index 0000000000..e8c4884950 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; +using Windows.ApplicationModel.DataTransfer; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages; + +internal sealed partial class ClipboardHistoryListPage : ListPage +{ + private readonly ObservableCollection clipboardHistory; + private readonly string _defaultIconPath; + + public ClipboardHistoryListPage() + { + clipboardHistory = []; + _defaultIconPath = string.Empty; + Icon = new("\uF0E3"); // ClipboardList icon + Name = "Clipboard History"; + Id = "com.microsoft.cmdpal.clipboardHistory"; + ShowDetails = true; + + Clipboard.HistoryChanged += TrackClipboardHistoryChanged_EventHandler; + } + + private void TrackClipboardHistoryChanged_EventHandler(object? sender, ClipboardHistoryChangedEventArgs? e) => RaiseItemsChanged(0); + + private bool IsClipboardHistoryEnabled() + { + var registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\"; + try + { + var enableClipboardHistory = (int)(Registry.GetValue(registryKey, "EnableClipboardHistory", false) ?? 0); + return enableClipboardHistory != 0; + } + catch (Exception) + { + return false; + } + } + + private bool IsClipboardHistoryDisabledByGPO() + { + var registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\"; + try + { + var allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null); + return allowClipboardHistory != null ? (int)allowClipboardHistory == 0 : false; + } + catch (Exception) + { + return false; + } + } + + private async Task LoadClipboardHistoryAsync() + { + try + { + List items = []; + + if (!Clipboard.IsHistoryEnabled()) + { + return; + } + + var historyItems = await Clipboard.GetHistoryItemsAsync(); + if (historyItems.Status != ClipboardHistoryItemsResultStatus.Success) + { + return; + } + + foreach (var item in historyItems.Items) + { + if (item.Content.Contains(StandardDataFormats.Text)) + { + var text = await item.Content.GetTextAsync(); + items.Add(new ClipboardItem { Content = text, Item = item }); + } + else if (item.Content.Contains(StandardDataFormats.Bitmap)) + { + items.Add(new ClipboardItem { Item = item }); + } + } + + clipboardHistory.Clear(); + + foreach (var item in items) + { + if (item.Item.Content.Contains(StandardDataFormats.Bitmap)) + { + var imageReceived = await item.Item.Content.GetBitmapAsync(); + + if (imageReceived != null) + { + item.ImageData = imageReceived; + } + } + + clipboardHistory.Add(item); + } + } + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + // Logger.LogError("Loading clipboard history failed", ex); + ExtensionHost.ShowStatus(new StatusMessage() { Message = "Loading clipboard history failed", State = MessageState.Error }, StatusContext.Page); + ExtensionHost.LogMessage(ex.ToString()); + } + } + + private void LoadClipboardHistoryInSTA() + { + // https://github.com/microsoft/windows-rs/issues/317 + // Clipboard API needs to be called in STA or it + // hangs. + var thread = new Thread(() => + { + var t = LoadClipboardHistoryAsync(); + t.ConfigureAwait(false); + t.Wait(); + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + } + + private ListItem[] GetClipboardHistoryListItems() + { + LoadClipboardHistoryInSTA(); + List listItems = []; + for (var i = 0; i < clipboardHistory.Count; i++) + { + var item = clipboardHistory[i]; + if (item != null) + { + listItems.Add(item.ToListItem()); + } + } + + return listItems.ToArray(); + } + + public override IListItem[] GetItems() => GetClipboardHistoryListItems(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.png new file mode 100644 index 0000000000..3696625cdc Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.svg new file mode 100644 index 0000000000..3b091d8680 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Assets/FileExplorer.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs new file mode 100644 index 0000000000..a93e421514 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs @@ -0,0 +1,38 @@ +// 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.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class CopyPathCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal CopyPathCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_CopyPath; + this.Icon = new IconInfo("\uE8c8"); + } + + public override CommandResult Invoke() + { + try + { + var dataPackage = new DataPackage(); + dataPackage.SetText(_item.FullPath); + Clipboard.SetContent(dataPackage); + Clipboard.Flush(); + } + catch + { + } + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs new file mode 100644 index 0000000000..478f03ef3a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs @@ -0,0 +1,44 @@ +// 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.Diagnostics; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenFileCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal OpenFileCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenFile; + this.Icon = Icons.OpenFile; + } + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.FileName = _item.FullPath; + process.StartInfo.UseShellExecute = true; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + Logger.LogError($"Unable to open {_item.FullPath}", ex); + } + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs new file mode 100644 index 0000000000..cd9c5ce94b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenInConsoleCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal OpenInConsoleCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenPathInConsole; + this.Icon = new IconInfo("\uE756"); + } + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath); + process.StartInfo.FileName = "cmd.exe"; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + Logger.LogError($"Unable to open {_item.FullPath}", ex); + } + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs new file mode 100644 index 0000000000..a6611cf6b3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenPropertiesCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + private static unsafe bool ShowFileProperties(string filename) + { + var propertiesPtr = Marshal.StringToHGlobalUni("properties"); + var filenamePtr = Marshal.StringToHGlobalUni(filename); + + try + { + var filenamePCWSTR = new PCWSTR((char*)filenamePtr); + var propertiesPCWSTR = new PCWSTR((char*)propertiesPtr); + + var info = new SHELLEXECUTEINFOW + { + cbSize = (uint)Marshal.SizeOf(), + lpVerb = propertiesPCWSTR, + lpFile = filenamePCWSTR, + nShow = (int)SHOW_WINDOW_CMD.SW_SHOW, + fMask = NativeHelpers.SEEMASKINVOKEIDLIST, + }; + + return PInvoke.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(propertiesPtr); + } + } + + internal OpenPropertiesCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenProperties; + this.Icon = new IconInfo("\uE90F"); + } + + public override CommandResult Invoke() + { + try + { + ShowFileProperties(_item.FullPath); + } + catch (Exception ex) + { + Logger.LogError("Error showing file properties: ", ex); + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs new file mode 100644 index 0000000000..a9c431d32c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenWithCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + private static unsafe bool OpenWith(string filename) + { + var filenamePtr = Marshal.StringToHGlobalUni(filename); + var verbPtr = Marshal.StringToHGlobalUni("openas"); + + try + { + var filenamePCWSTR = new PCWSTR((char*)filenamePtr); + var verbPCWSTR = new PCWSTR((char*)verbPtr); + + var info = new SHELLEXECUTEINFOW + { + cbSize = (uint)Marshal.SizeOf(), + lpVerb = verbPCWSTR, + lpFile = filenamePCWSTR, + nShow = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL, + fMask = NativeHelpers.SEEMASKINVOKEIDLIST, + }; + + return PInvoke.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(verbPtr); + } + } + + internal OpenWithCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenWith; + this.Icon = new IconInfo("\uE7AC"); + } + + public override CommandResult Invoke() + { + OpenWith(_item.FullPath); + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs new file mode 100644 index 0000000000..5b65cc9ef8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; + +namespace Microsoft.CmdPal.Ext.Indexer.Data; + +internal sealed class IndexerItem +{ + internal string FullPath { get; init; } + + internal string FileName { get; init; } + + internal bool IsDirectory() + { + if (!Path.Exists(FullPath)) + { + return false; + } + + var attr = File.GetAttributes(FullPath); + + // detect whether it is a directory or file + return (attr & FileAttributes.Directory) == FileAttributes.Directory; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs new file mode 100644 index 0000000000..57d399b668 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Indexer.Commands; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Indexer.Data; + +internal sealed partial class IndexerListItem : ListItem +{ + internal string FilePath { get; private set; } + + public IndexerListItem( + IndexerItem indexerItem, + IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include) + : base(new OpenFileCommand(indexerItem)) + { + FilePath = indexerItem.FullPath; + + Title = indexerItem.FileName; + Subtitle = indexerItem.FullPath; + List context = []; + if (indexerItem.IsDirectory()) + { + var directoryPage = new DirectoryPage(indexerItem.FullPath); + if (browseByDefault == IncludeBrowseCommand.AsDefault) + { + // Swap the open file command into the context menu + context.Add(new CommandContextItem(Command)); + Command = directoryPage; + } + else if (browseByDefault == IncludeBrowseCommand.Include) + { + context.Add(new CommandContextItem(directoryPage)); + } + } + + MoreCommands = [ + ..context, + new CommandContextItem(new OpenWithCommand(indexerItem)), + new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }), + new CommandContextItem(new CopyPathCommand(indexerItem)), + new CommandContextItem(new OpenInConsoleCommand(indexerItem)), + new CommandContextItem(new OpenPropertiesCommand(indexerItem)), + ]; + } +} + +internal enum IncludeBrowseCommand +{ + AsDefault = 0, + Include = 1, + Exclude = 2, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs new file mode 100644 index 0000000000..e186251825 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs @@ -0,0 +1,49 @@ +// 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 ManagedCommon; +using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.Search; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal static class DataSourceManager +{ + private static readonly Guid CLSIDCollatorDataSource = new("9E175B8B-F52A-11D8-B9A5-505054503030"); + + private static IDBInitialize _dataSource; + + public static IDBInitialize GetDataSource() + { + if (_dataSource == null) + { + InitializeDataSource(); + } + + return _dataSource; + } + + private static bool InitializeDataSource() + { + var hr = PInvoke.CoCreateInstance(CLSIDCollatorDataSource, null, CLSCTX.CLSCTX_INPROC_SERVER, typeof(IDBInitialize).GUID, out var dataSourceObj); + if (hr != 0) + { + Logger.LogError("CoCreateInstance failed: " + hr); + return false; + } + + if (dataSourceObj == null) + { + Logger.LogError("CoCreateInstance failed: dataSourceObj is null"); + return false; + } + + _dataSource = (IDBInitialize)dataSourceObj; + _dataSource.Initialize(); + + return true; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs new file mode 100644 index 0000000000..d4be1b967f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Windows.Win32.Storage.IndexServer; +using Windows.Win32.System.Com.StructuredStorage; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +internal struct DBPROP +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public uint dwPropertyID; + public uint dwOptions; + public uint dwStatus; + public DBID colid; + public PROPVARIANT vValue; +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs new file mode 100644 index 0000000000..9226e76757 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +public struct DBPROPIDSET +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public IntPtr rgPropertyIDs; // Pointer to array of property IDs + public uint cPropertyIDs; // Number of properties in array + public Guid guidPropertySet; // GUID of the property set +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs new file mode 100644 index 0000000000..7b38b9debe --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +public struct DBPROPSET +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public IntPtr rgProperties; // Pointer to an array of DBPROP + public uint cProperties; // Number of properties in the array + public Guid guidPropertySet; // GUID of the property set +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs new file mode 100644 index 0000000000..3127e2e563 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[ComImport] +[Guid("0c733a7c-2a1c-11ce-ade5-00aa0044773d")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IRowset +{ + [PreserveSig] + int AddRefRows( + uint cRows, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus); + + [PreserveSig] + int GetData( + IntPtr hRow, + IntPtr hAccessor, + IntPtr pData); + + [PreserveSig] + int GetNextRows( + IntPtr hReserved, + long lRowsOffset, + long cRows, + out uint pcRowsObtained, + out IntPtr prghRows); + + [PreserveSig] + int ReleaseRows( + uint cRows, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows, + IntPtr rgRowOptions, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs new file mode 100644 index 0000000000..5c891c8036 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[ComImport] +[Guid("0C733A55-2A1C-11CE-ADE5-00AA0044773D")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IRowsetInfo +{ + [PreserveSig] + int GetProperties( + uint cPropertyIDSets, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] DBPROPIDSET[] rgPropertyIDSets, + out ulong pcPropertySets, + out IntPtr prgPropertySets); + + [PreserveSig] + int GetReferencedRowset( + uint iOrdinal, + [In] ref Guid riid, + [Out, MarshalAs(UnmanagedType.Interface)] out object ppReferencedRowset); + + [PreserveSig] + int GetSpecification( + [In] ref Guid riid, + [Out, MarshalAs(UnmanagedType.Interface)] out object ppSpecification); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs new file mode 100644 index 0000000000..1840338b73 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs @@ -0,0 +1,96 @@ +// 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 ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Windows.Win32.System.Com; +using Windows.Win32.System.Com.StructuredStorage; +using Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal sealed class SearchResult +{ + public string ItemDisplayName { get; init; } + + public string ItemUrl { get; init; } + + public string LaunchUri { get; init; } + + public bool IsFolder { get; init; } + + public SearchResult(string name, string url, string filePath, bool isFolder) + { + ItemDisplayName = name; + ItemUrl = url; + IsFolder = isFolder; + + if (LaunchUri == null || LaunchUri.Length == 0) + { + // Launch the file with the default app, so use the file path + LaunchUri = filePath; + } + } + + public static unsafe SearchResult Create(IPropertyStore propStore) + { + try + { + var key = NativeHelpers.PropertyKeys.PKEYItemNameDisplay; + propStore.GetValue(&key, out var itemNameDisplay); + + key = NativeHelpers.PropertyKeys.PKEYItemUrl; + propStore.GetValue(&key, out var itemUrl); + + key = NativeHelpers.PropertyKeys.PKEYKindText; + propStore.GetValue(&key, out var kindText); + + var filePath = GetFilePath(ref itemUrl); + var isFolder = IsFoder(ref kindText); + + // Create the actual result object + var searchResult = new SearchResult( + GetStringFromPropVariant(ref itemNameDisplay), + GetStringFromPropVariant(ref itemUrl), + filePath, + isFolder); + + return searchResult; + } + catch (Exception ex) + { + Logger.LogError("Failed to get property values from propStore.", ex); + return null; + } + } + + private static bool IsFoder(ref PROPVARIANT kindText) + { + var kindString = GetStringFromPropVariant(ref kindText); + return string.Equals(kindString, "Folder", StringComparison.OrdinalIgnoreCase); + } + + private static string GetFilePath(ref PROPVARIANT itemUrl) + { + var filePath = GetStringFromPropVariant(ref itemUrl); + filePath = UrlToFilePathConverter.Convert(filePath); + return filePath; + } + + private static string GetStringFromPropVariant(ref PROPVARIANT propVariant) + { + if (propVariant.Anonymous.Anonymous.vt == VARENUM.VT_LPWSTR) + { + var pwszVal = propVariant.Anonymous.Anonymous.Anonymous.pwszVal; + if (pwszVal != null) + { + return pwszVal.ToString(); + } + } + + return string.Empty; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs new file mode 100644 index 0000000000..f835acafaa --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs @@ -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.Globalization; +using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; + +internal sealed class QueryStringBuilder +{ + private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText"; + private const string SystemIndex = "SystemIndex"; + private const string ScopeFileConditions = "SCOPE='file:'"; + private const string OrderConditions = "System.DateModified DESC"; + private const string SelectQueryWithScope = "SELECT " + Properties + " FROM " + SystemIndex + " WHERE (" + ScopeFileConditions + ")"; + private const string SelectQueryWithScopeAndOrderConditions = SelectQueryWithScope + " ORDER BY " + OrderConditions; + + private static ISearchQueryHelper queryHelper; + + public static string GeneratePrimingQuery() => SelectQueryWithScopeAndOrderConditions; + + public static string GenerateQuery(string searchText, uint whereId) + { + if (queryHelper == null) + { + var searchManager = new CSearchManager(); + ISearchCatalogManager catalogManager = searchManager.GetCatalog(SystemIndex); + queryHelper = catalogManager.GetQueryHelper(); + + queryHelper.QuerySelectColumns = Properties; + queryHelper.QueryContentProperties = "System.FileName"; + queryHelper.QuerySorting = OrderConditions; + } + + queryHelper.QueryWhereRestrictions = "AND " + ScopeFileConditions + "AND ReuseWhere(" + whereId.ToString(CultureInfo.InvariantCulture) + ")"; + return queryHelper.GenerateSQLFromUserQuery(searchText); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs new file mode 100644 index 0000000000..bbfb8554a6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; + +public class UrlToFilePathConverter +{ + public static string Convert(string url) + { + var result = url.Replace('/', '\\'); // replace all '/' to '\' + + var fileProtocolString = "file:"; + var indexProtocolFound = url.IndexOf(fileProtocolString, StringComparison.CurrentCultureIgnoreCase); + + if (indexProtocolFound != -1 && (indexProtocolFound + fileProtocolString.Length) < url.Length) + { + result = result[(indexProtocolFound + fileProtocolString.Length)..]; + } + + return result; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj new file mode 100644 index 0000000000..70e6a10c5a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj @@ -0,0 +1,49 @@ + + + + Microsoft.CmdPal.Ext.Indexer + + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + True + True + Resources.resx + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs new file mode 100644 index 0000000000..9851573843 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs @@ -0,0 +1,25 @@ +// 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 Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Microsoft.CmdPal.Ext.Indexer.Native; + +internal sealed class NativeHelpers +{ + public const uint SEEMASKINVOKEIDLIST = 12; + + internal static class PropertyKeys + { + public static readonly PROPERTYKEY PKEYItemNameDisplay = new() { fmtid = new System.Guid("B725F130-47EF-101A-A5F1-02608C9EEBAC"), pid = 10 }; + public static readonly PROPERTYKEY PKEYItemUrl = new() { fmtid = new System.Guid("49691C90-7E17-101A-A91C-08002B2ECDA9"), pid = 9 }; + public static readonly PROPERTYKEY PKEYKindText = new() { fmtid = new System.Guid("F04BEF95-C585-4197-A2B7-DF46FDC9EE6D"), pid = 100 }; + } + + internal static class OleDb + { + public static readonly Guid DbGuidDefault = new("C8B521FB-5CF3-11CE-ADE5-00AA0044773D"); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs new file mode 100644 index 0000000000..531398685b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +#nullable enable +namespace Microsoft.CmdPal.Ext.Indexer; + +/// +/// This is almost more of just a sample than anything. +/// This is one singular page for switching. +/// +public sealed partial class DirectoryExplorePage : DynamicListPage +{ + private string _path; + private List? _directoryContents; + private List? _filteredContents; + + public DirectoryExplorePage(string path) + { + _path = path; + Icon = Icons.FileExplorer; + Name = Resources.Indexer_Command_Browse; + Title = path; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (_directoryContents == null) + { + return; + } + + if (string.IsNullOrEmpty(newSearch)) + { + if (_filteredContents != null) + { + _filteredContents = null; + RaiseItemsChanged(-1); + } + + return; + } + + // Need to break this out the manual way so that the compiler can know + // this is an ExploreListItem + var filteredResults = ListHelpers.FilterList( + _directoryContents, + newSearch, + (s, i) => ListHelpers.ScoreListItem(s, i)); + + if (_filteredContents != null) + { + lock (_filteredContents) + { + ListHelpers.InPlaceUpdateList(_filteredContents, filteredResults); + } + } + else + { + _filteredContents = filteredResults.ToList(); + } + + RaiseItemsChanged(-1); + } + + public override IListItem[] GetItems() + { + if (_filteredContents != null) + { + return _filteredContents.ToArray(); + } + + if (_directoryContents != null) + { + return _directoryContents.ToArray(); + } + + IsLoading = true; + if (!Path.Exists(_path)) + { + EmptyContent = new CommandItem(title: Resources.Indexer_File_Does_Not_Exist); + return []; + } + + var attr = File.GetAttributes(_path); + + // detect whether its a directory or file + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + EmptyContent = new CommandItem(title: Resources.Indexer_File_Is_File_Not_Folder); + return []; + } + + var contents = Directory.EnumerateFileSystemEntries(_path); + _directoryContents = contents + .Select(s => new IndexerItem() { FullPath = s, FileName = Path.GetFileName(s) }) + .Select(i => new ExploreListItem(i)) + .ToList(); + + foreach (var i in _directoryContents) + { + i.PathChangeRequested += HandlePathChangeRequested; + } + + _ = Task.Run(() => + { + foreach (var item in _directoryContents) + { + IconInfo? icon = null; + try + { + var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result; + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + icon = new IconInfo(data, data); + } + } + catch + { + } + + item.Icon = icon; + } + }); + + IsLoading = false; + + return _directoryContents.ToArray(); + } + + private void HandlePathChangeRequested(ExploreListItem sender, string path) + { + _directoryContents = null; + _filteredContents = null; + _path = path; + Title = path; + SearchText = string.Empty; + RaiseItemsChanged(-1); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs new file mode 100644 index 0000000000..501e2b96f3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Indexer.Commands; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +#nullable enable +namespace Microsoft.CmdPal.Ext.Indexer; + +/// +/// This is almost more of just a sample than anything. +/// +internal sealed partial class ExploreListItem : ListItem +{ + internal string FilePath { get; private set; } + + internal event TypedEventHandler? PathChangeRequested; + + public ExploreListItem(IndexerItem indexerItem) + : base(new NoOpCommand()) + { + FilePath = indexerItem.FullPath; + + Title = indexerItem.FileName; + Subtitle = indexerItem.FullPath; + List context = []; + if (indexerItem.IsDirectory()) + { + Command = new AnonymousCommand( + () => { PathChangeRequested?.Invoke(this, FilePath); }) + { + Result = CommandResult.KeepOpen(), + Name = Resources.Indexer_Command_Browse, + }; + context.Add(new CommandContextItem(new DirectoryPage(indexerItem.FullPath))); + } + else + { + Command = new OpenFileCommand(indexerItem); + } + + MoreCommands = [ + ..context, + new CommandContextItem(new OpenWithCommand(indexerItem)), + new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }), + new CommandContextItem(new CopyPathCommand(indexerItem)), + new CommandContextItem(new OpenInConsoleCommand(indexerItem)), + new CommandContextItem(new OpenPropertiesCommand(indexerItem)), + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs new file mode 100644 index 0000000000..3d277d9dcf --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Indexer; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.Indexer; + +internal sealed partial class IndexerPage : DynamicListPage, IDisposable +{ + private readonly List _indexerListItems = []; + + private SearchQuery _searchQuery = new(); + + private uint _queryCookie = 10; + + public IndexerPage() + { + Id = "com.microsoft.indexer.fileSearch"; + Icon = Icons.FileExplorer; + Name = Resources.Indexer_Title; + PlaceholderText = Resources.Indexer_PlaceholderText; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (oldSearch != newSearch) + { + _ = Task.Run(() => + { + Query(newSearch); + LoadMore(); + }); + } + } + + public override IListItem[] GetItems() => [.. _indexerListItems]; + + public override void LoadMore() + { + IsLoading = true; + FetchItems(20); + IsLoading = false; + RaiseItemsChanged(_indexerListItems.Count); + } + + private void Query(string query) + { + ++_queryCookie; + _indexerListItems.Clear(); + _searchQuery.SearchResults.Clear(); + _searchQuery.CancelOutstandingQueries(); + + if (query == string.Empty) + { + return; + } + + Stopwatch stopwatch = new(); + stopwatch.Start(); + + _searchQuery.Execute(query, _queryCookie); + + stopwatch.Stop(); + Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\""); + } + + private void FetchItems(int limit) + { + if (_searchQuery != null) + { + var cookie = _searchQuery.Cookie; + if (cookie == _queryCookie) + { + var index = 0; + SearchResult result; + + var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit); + + while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit) + { + IconInfo icon = null; + try + { + var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result; + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + icon = new IconInfo(data, data); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to get the icon.", ex); + } + + _indexerListItems.Add(new IndexerListItem(new IndexerItem + { + FileName = result.ItemDisplayName, + FullPath = result.LaunchUri, + }) + { + Icon = icon, + }); + } + + HasMoreItems = hasMoreItems; + } + } + } + + public void Dispose() + { + _searchQuery = null; + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.png new file mode 100644 index 0000000000..fe2f68e2fa Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.svg new file mode 100644 index 0000000000..b9d416dbeb --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Assets/Registry.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Classes/RegistryEntry.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Classes/RegistryEntry.cs new file mode 100644 index 0000000000..2f936ac667 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Classes/RegistryEntry.cs @@ -0,0 +1,116 @@ +// 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.Registry.Helpers; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Registry.Classes; + +/// +/// A entry of the registry. +/// +internal sealed class RegistryEntry +{ +#pragma warning disable CS8632 + /// + /// Gets the full path to a registry key. + /// + internal string KeyPath { get; } + + /// + /// Gets the registry key for this entry. + /// + internal RegistryKey? Key { get; } + + /// + /// Gets a possible exception that was occurred when try to open this registry key (e.g. ). + /// + internal Exception? Exception { get; } + + /// + /// Gets the name of the current selected registry value. + /// + internal string? ValueName { get; } + + /// + /// Gets the value of the current selected registry value. + /// + internal object? ValueData { get; } + +#pragma warning restore CS8632 + + /// + /// Initializes a new instance of the class. + /// + /// The full path to the registry key for this entry. + /// A exception that was occurred when try to access this registry key. + internal RegistryEntry(string keyPath, Exception exception) + { + KeyPath = keyPath; + Exception = exception; + } + + /// + /// Initializes a new instance of the class. + /// + /// The for this entry. + internal RegistryEntry(RegistryKey key) + { + KeyPath = key.Name; + Key = key; + } + + /// + /// Initializes a new instance of the class. + /// + /// The for this entry. + /// The value name of the current selected registry value. + /// The value of the current selected registry value. + internal RegistryEntry(RegistryKey key, string valueName, object value) + { + KeyPath = key.Name; + Key = key; + ValueName = valueName; + ValueData = value; + } + + /// + /// Return the registry key. + /// + /// A registry key. + internal string GetRegistryKey() + { + return $"{Key?.Name ?? KeyPath}"; + } + + /// + /// Return the value name with the complete registry key. + /// + /// A value name with the complete registry key. + internal string GetValueNameWithKey() + { + return $"{Key?.Name ?? KeyPath}\\\\{ValueName?.ToString() ?? string.Empty}"; + } + + /// + /// Return the value data of a value name inside a registry key. + /// + /// A value data. + internal string GetValueData() + { + if (Key is null) + { + return KeyPath; + } + + if (string.IsNullOrEmpty(ValueName)) + { + return Key.Name; + } + + return ValueHelper.GetValue(Key, ValueName); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs new file mode 100644 index 0000000000..d876eb7efc --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs @@ -0,0 +1,49 @@ +// 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.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.Registry.Commands; + +internal sealed partial class CopyRegistryInfoCommand : InvokableCommand +{ + private readonly RegistryEntry _entry; + private readonly string _stringToCopy; + + internal CopyRegistryInfoCommand(RegistryEntry entry, CopyType typeToCopy) + { + if (typeToCopy == CopyType.Key) + { + Name = Resources.CopyKeyNamePath; + Icon = new IconInfo("\xE8C8"); // Copy Icon + _stringToCopy = entry.GetRegistryKey(); + } + else if (typeToCopy == CopyType.ValueData) + { + Name = Resources.CopyValueData; + Icon = new IconInfo("\xF413"); // CopyTo Icon + _stringToCopy = entry.GetValueData(); + } + else if (typeToCopy == CopyType.ValueName) + { + Name = Resources.CopyValueName; + Icon = new IconInfo("\xE8C8"); // Copy Icon + _stringToCopy = entry.GetValueNameWithKey(); + } + + _entry = entry; + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetText(_stringToCopy); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/OpenKeyInEditorCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/OpenKeyInEditorCommand.cs new file mode 100644 index 0000000000..d10b994318 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/OpenKeyInEditorCommand.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Helpers; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.Registry.Commands; + +internal sealed partial class OpenKeyInEditorCommand : InvokableCommand +{ + private readonly RegistryEntry _entry; + + internal OpenKeyInEditorCommand(RegistryEntry entry) + { + Name = Resources.OpenKeyInRegistryEditor; + Icon = new IconInfo("\xE8A7"); // OpenInNewWindow icon + _entry = entry; + } + + internal static bool TryToOpenInRegistryEditor(in RegistryEntry entry) + { + try + { + RegistryHelper.OpenRegistryKey(entry.Key?.Name ?? entry.KeyPath); + return true; + } + catch (System.ComponentModel.Win32Exception) + { + // TODO GH #118 We need a convenient way to show errors to a user + // MessageBox.Show( + // Resources.OpenInRegistryEditorAccessExceptionText, + // Resources.OpenInRegistryEditorAccessExceptionTitle, + // MessageBoxButton.OK, + // MessageBoxImage.Error); + return false; + } +#pragma warning disable CS0168, IDE0059 + catch (Exception exception) + { + // TODO GH #108: Logging + // Log.Exception("Error on opening Windows registry editor", exception, typeof(Main)); + return false; + } +#pragma warning restore CS0168, IDE0059 + } + + public override CommandResult Invoke() + { + TryToOpenInRegistryEditor(_entry); + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/KeyName.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/KeyName.cs new file mode 100644 index 0000000000..a95aaaa7a0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/KeyName.cs @@ -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. + +namespace Microsoft.CmdPal.Ext.Registry.Constants; + +/// +/// This class contains names for important registry keys +/// +internal static class KeyName +{ + /// + /// The first name part of each base key without the underscore + /// + internal const string FirstPart = "HKEY"; + + /// + /// The first name part of each base key follow by a underscore + /// + internal const string FirstPartUnderscore = "HKEY_"; + + /// + /// The short name for the base key HKEY_CLASSES_ROOT (see ) + /// + internal const string ClassRootShort = "HKCR"; + + /// + /// The short name for the base key HKEY_CURRENT_CONFIG (see ) + /// + internal const string CurrentConfigShort = "HKCC"; + + /// + /// The short name for the base key HKEY_CURRENT_USER (see ) + /// + internal const string CurrentUserShort = "HKCU"; + + /// + /// The short name for the base key HKEY_LOCAL_MACHINE (see ) + /// + internal const string LocalMachineShort = "HKLM"; + + /// + /// The short name for the base key HKEY_PERFORMANCE_DATA (see ) + /// + internal const string PerformanceDataShort = "HKPD"; + + /// + /// The short name for the base key HKEY_USERS (see ) + /// + internal const string UsersShort = "HKU"; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/MaxTextLength.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/MaxTextLength.cs new file mode 100644 index 0000000000..e16417bcf3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Constants/MaxTextLength.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Registry.Constants; + +/// +/// This class contain all maximum text length. +/// +public static class MaxTextLength +{ + /// + /// The maximum length for the title text length with two context menu symbols on the right. + /// + internal const int MaximumTitleLengthWithTwoSymbols = 44; + + /// + /// The maximum length for the title text length with three context menu symbols on the right. + /// + internal const int MaximumTitleLengthWithThreeSymbols = 40; + + /// + /// The maximum length for the sub-title text length with two context menu symbols on the right. + /// + internal const int MaximumSubTitleLengthWithTwoSymbols = 85; + + /// + /// The maximum length for the sub-title text length with three context menu symbols on the right. + /// + internal const int MaximumSubTitleLengthWithThreeSymbols = 78; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/CopyType.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/CopyType.cs new file mode 100644 index 0000000000..cfcf2e3337 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/CopyType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Registry; + +public enum CopyType +{ + Key, + ValueData, + ValueName, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Enumerations/TruncateSide.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Enumerations/TruncateSide.cs new file mode 100644 index 0000000000..298000c35f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Enumerations/TruncateSide.cs @@ -0,0 +1,21 @@ +// 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.Registry.Enumerations; + +/// +/// The truncate side for a to long text +/// +internal enum TruncateSide +{ + /// + /// Truncate a text only from the right side + /// + OnlyFromLeft, + + /// + /// Truncate a text only from the left side + /// + OnlyFromRight, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ContextMenuHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ContextMenuHelper.cs new file mode 100644 index 0000000000..4d10203e11 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ContextMenuHelper.cs @@ -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 System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Input; +using Microsoft.CmdPal.Ext.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Commands; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +/// +/// Helper class to easier work with context menu entries +/// +internal static class ContextMenuHelper +{ + /// + /// Return a list with all context menu entries for the given + /// Symbols taken from + /// + internal static List GetContextMenu(RegistryEntry entry) + { + var list = new List(); + + if (string.IsNullOrEmpty(entry.ValueName)) + { + list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.Key))); + } + else + { + list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.ValueData))); + list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.ValueName))); + } + + // list.Add(new CommandContextItem(new OpenKeyInEditorCommand(entry))); + return list; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/QueryHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/QueryHelper.cs new file mode 100644 index 0000000000..d197038eb8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/QueryHelper.cs @@ -0,0 +1,116 @@ +// 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.RegularExpressions; + +using Microsoft.CmdPal.Ext.Registry.Constants; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +/// +/// Helper class to easier work with queries +/// +internal static partial class QueryHelper +{ + /// + /// The character to distinguish if the search query contain multiple parts (typically "\\") + /// + internal const string QuerySplitCharacter = "\\\\"; + + /// + /// A list that contain short names of all registry base keys + /// + private static readonly IReadOnlyDictionary _shortBaseKeys = new Dictionary(6) + { + { Win32.Registry.ClassesRoot.Name, KeyName.ClassRootShort }, + { Win32.Registry.CurrentConfig.Name, KeyName.CurrentConfigShort }, + { Win32.Registry.CurrentUser.Name, KeyName.CurrentUserShort }, + { Win32.Registry.LocalMachine.Name, KeyName.LocalMachineShort }, + { Win32.Registry.PerformanceData.Name, KeyName.PerformanceDataShort }, + { Win32.Registry.Users.Name, KeyName.UsersShort }, + }; + + [GeneratedRegex(@"/(?<=^(?:[^""]*""[^""]*"")*[^""]*)(? + /// Sanitize the query to avoid issues with the regex + /// + /// Query containing front-slash + /// A string replacing all the front-slashes with back-slashes + private static string SanitizeQuery(in string query) + { + var sanitizedQuery = FrontToBackSlashRegex().Replace(query, "\\"); + + return sanitizedQuery.Replace("\"", string.Empty); + } + + /// + /// Return the parts of a given query + /// + /// The query that could contain parts + /// The key part of the query + /// The value name part of the query + /// when the query search for a key and a value name, otherwise + internal static bool GetQueryParts(in string query, out string queryKey, out string queryValueName) + { + var sanitizedQuery = SanitizeQuery(query); + + if (!sanitizedQuery.Contains(QuerySplitCharacter, StringComparison.InvariantCultureIgnoreCase)) + { + queryKey = sanitizedQuery; + queryValueName = string.Empty; + return false; + } + + var querySplit = sanitizedQuery.Split(QuerySplitCharacter); + + queryKey = querySplit.First(); + queryValueName = querySplit.Last(); + return true; + } + + /// + /// Return a registry key with a long base key + /// + /// A registry key with a short base key + /// A registry key with a long base key + internal static string GetKeyWithLongBaseKey(in string registryKey) + { + foreach (var shortName in _shortBaseKeys) + { + if (!registryKey.StartsWith(shortName.Value, StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + + return registryKey.Replace(shortName.Value, shortName.Key, StringComparison.InvariantCultureIgnoreCase); + } + + return registryKey; + } + + /// + /// Return a registry key with a short base key (useful to reduce the text length of a registry key) + /// + /// A registry key with a full base key + /// A registry key with a short base key + internal static string GetKeyWithShortBaseKey(in string registryKey) + { + foreach (var shortName in _shortBaseKeys) + { + if (!registryKey.StartsWith(shortName.Key, StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + + return registryKey.Replace(shortName.Key, shortName.Value, StringComparison.InvariantCultureIgnoreCase); + } + + return registryKey; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs new file mode 100644 index 0000000000..0f8cea7966 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/RegistryHelper.cs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; + +using Microsoft.CmdPal.Ext.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Constants; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +/// +/// Helper class to easier work with the registry +/// +internal static class RegistryHelper +{ + /// + /// A list that contain all registry base keys in a long/full version and in a short version (e.g HKLM = HKEY_LOCAL_MACHINE) + /// + private static readonly IReadOnlyDictionary _baseKeys = new Dictionary(12) + { + { KeyName.ClassRootShort, Win32.Registry.ClassesRoot }, + { Win32.Registry.ClassesRoot.Name, Win32.Registry.ClassesRoot }, + { KeyName.CurrentConfigShort, Win32.Registry.CurrentConfig }, + { Win32.Registry.CurrentConfig.Name, Win32.Registry.CurrentConfig }, + { KeyName.CurrentUserShort, Win32.Registry.CurrentUser }, + { Win32.Registry.CurrentUser.Name, Win32.Registry.CurrentUser }, + { KeyName.LocalMachineShort, Win32.Registry.LocalMachine }, + { Win32.Registry.LocalMachine.Name, Win32.Registry.LocalMachine }, + { KeyName.PerformanceDataShort, Win32.Registry.PerformanceData }, + { Win32.Registry.PerformanceData.Name, Win32.Registry.PerformanceData }, + { KeyName.UsersShort, Win32.Registry.Users }, + { Win32.Registry.Users.Name, Win32.Registry.Users }, + }; + + /// + /// Try to find registry base keys based on the given query + /// + /// The query to search + /// A combination of a list of base and the sub keys +#pragma warning disable CS8632 + internal static (IEnumerable? BaseKey, string SubKey) GetRegistryBaseKey(in string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + return (null, string.Empty); + } + + var baseKey = query.Split('\\').FirstOrDefault() ?? string.Empty; + var subKey = query.Replace(baseKey, string.Empty, StringComparison.InvariantCultureIgnoreCase).TrimStart('\\'); + + var baseKeyResult = _baseKeys + .Where(found => found.Key.StartsWith(baseKey, StringComparison.InvariantCultureIgnoreCase)) + .Select(found => found.Value) + .Distinct(); + + return (baseKeyResult, subKey); + } + + /// + /// Return a list of all registry base key + /// + /// A list with all registry base keys + internal static ICollection GetAllBaseKeys() + { + return new Collection + { + new(Win32.Registry.ClassesRoot), + new(Win32.Registry.CurrentConfig), + new(Win32.Registry.CurrentUser), + new(Win32.Registry.LocalMachine), + new(Win32.Registry.PerformanceData), + new(Win32.Registry.Users), + }; + } + + /// + /// Search for the given sub-key path in the given registry base key + /// + /// The base + /// The path of the registry sub-key + /// A list with all found registry keys + internal static ICollection SearchForSubKey(in RegistryKey baseKey, in string subKeyPath) + { + if (string.IsNullOrEmpty(subKeyPath)) + { + return FindSubKey(baseKey, string.Empty); + } + + var subKeysNames = subKeyPath.Split('\\'); + var index = 0; + RegistryKey? subKey = baseKey; +#pragma warning restore CS8632 + ICollection result; + + do + { + result = FindSubKey(subKey, subKeysNames.ElementAtOrDefault(index) ?? string.Empty); + + if (result.Count == 0) + { + // If a subKey can't be found, show no results. + break; + } + + if (result.Count == 1 && index < subKeysNames.Length) + { + subKey = result.First().Key; + } + + if (result.Count > 1 || subKey == null) + { + break; + } + + index++; + } + while (index < subKeysNames.Length); + + return result; + } + + /// + /// Return a human readable summary of a given + /// + /// The for the summary + /// A human readable summary + internal static string GetSummary(in RegistryKey key) + { + return $"{Resources.SubKeys} {key.SubKeyCount} - {Resources.Values} {key.ValueCount}"; + } + + /// + /// Open a given registry key in the registry editor + /// + /// The registry key to open + internal static void OpenRegistryKey(in string fullKey) + { + // Set the registry key + Win32.Registry.SetValue(@"HKEY_Current_User\Software\Microsoft\Windows\CurrentVersion\Applets\Regedit", "LastKey", fullKey); + + // Open regedit.exe with multi-instance option + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "regedit.exe", + Arguments = "-m", + UseShellExecute = true, + Verb = "runas", // Runs as Administrator + }; + + Process.Start(startInfo); + } + + /// + /// Try to find the given registry sub-key in the given registry parent-key + /// + /// The parent-key, also the root to start the search + /// The sub-key to find + /// A list with all found registry sub-keys + private static Collection FindSubKey(in RegistryKey parentKey, in string searchSubKey) + { + var list = new Collection(); + + try + { + foreach (var subKey in parentKey.GetSubKeyNames().OrderBy(found => found)) + { + if (!subKey.StartsWith(searchSubKey, StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + + if (string.Equals(subKey, searchSubKey, StringComparison.OrdinalIgnoreCase)) + { + var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree); + if (key != null) + { + list.Add(new RegistryEntry(key)); + } + + return list; + } + + try + { + var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree); + if (key != null) + { + list.Add(new RegistryEntry(key)); + } + } + catch (Exception exception) + { + list.Add(new RegistryEntry($"{parentKey.Name}\\{subKey}", exception)); + } + } + } + catch (Exception ex) + { + list.Add(new RegistryEntry(parentKey.Name, ex)); + } + + return list; + } + + /// + /// Return a list with a registry sub-keys of the given registry parent-key + /// + /// The registry parent-key + /// (optional) The maximum count of the results + /// A list with all found registry sub-keys + private static Collection GetAllSubKeys(in RegistryKey parentKey, in int maxCount = 50) + { + var list = new Collection(); + + try + { + foreach (var subKey in parentKey.GetSubKeyNames()) + { + if (list.Count >= maxCount) + { + break; + } + + list.Add(new RegistryEntry(parentKey)); + } + } + catch (Exception exception) + { + list.Add(new RegistryEntry(parentKey.Name, exception)); + } + + return list; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs new file mode 100644 index 0000000000..791d242d00 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ResultHelper.cs @@ -0,0 +1,205 @@ +// 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.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Commands; +using Microsoft.CmdPal.Ext.Registry.Constants; +using Microsoft.CmdPal.Ext.Registry.Enumerations; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +/// +/// Helper class to easier work with results +/// +internal static class ResultHelper +{ + /// + /// Return a list with s, based on the given list + /// + /// The original result list to convert + /// A list with + internal static List GetResultList(in IEnumerable list) + { + var resultList = new List(); + + foreach (var entry in list) + { + var result = new ListItem(new OpenKeyInEditorCommand(entry)) + { + Icon = RegistryListPage.RegistryIcon, + MoreCommands = ContextMenuHelper.GetContextMenu(entry).ToArray(), + }; + + if (entry.Exception is null && entry.Key is not null) + { + // when key contains keys or fields + result.TextToSuggest = entry.Key.Name; + result.Subtitle = RegistryHelper.GetSummary(entry.Key); + result.Title = GetTruncatedText(entry.Key.Name, MaxTextLength.MaximumTitleLengthWithTwoSymbols); + } + else if (entry.Key is null && entry.Exception is not null) + { + // on error (e.g access denied) + result.TextToSuggest = entry.KeyPath; + result.Subtitle = GetTruncatedText(entry.Exception.Message, MaxTextLength.MaximumSubTitleLengthWithTwoSymbols, TruncateSide.OnlyFromRight); + result.Title = GetTruncatedText(entry.KeyPath, MaxTextLength.MaximumTitleLengthWithTwoSymbols); + } + else + { + result.TextToSuggest = entry.KeyPath; + result.Title = GetTruncatedText(entry.KeyPath, MaxTextLength.MaximumTitleLengthWithTwoSymbols); + } + + // result.ContextData = entry; + // TODO GH #126 Investigate tool tips, result.ToolTipData = new ToolTipData(Resources.RegistryKey, $"{Resources.KeyName} {result.Title}"); + resultList.Add(result); + } + + return resultList; + } + +#pragma warning disable CS8632 + internal static List GetValuesFromKey(in RegistryKey? key, string searchValue = "") + { +#pragma warning restore CS8632 + if (key is null) + { + return []; + } + + var valueList = new List>(key.ValueCount); + + var resultList = new List(); + + try + { + var valueNames = key.GetValueNames(); + + try + { + foreach (var valueName in valueNames) + { + var value = key.GetValue(valueName); + if (value != null) + { + valueList.Add(KeyValuePair.Create(valueName, value)); + } + } + } + catch (Exception valueException) + { + var registryEntry = new RegistryEntry(key.Name, valueException); + + resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry)) + { + Icon = RegistryListPage.RegistryIcon, + Subtitle = GetTruncatedText(valueException.Message, MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight), + Title = GetTruncatedText(key.Name, MaxTextLength.MaximumTitleLengthWithThreeSymbols), + MoreCommands = ContextMenuHelper.GetContextMenu(registryEntry).ToArray(), + + // TODO --> Investigate ToolTipData = new ToolTipData(valueException.Message, valueException.ToString()), + }); + } + + if (!string.IsNullOrEmpty(searchValue)) + { + var filteredValueName = valueList.Where(found => found.Key.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase)); + var filteredValueList = valueList.Where(found => found.Value.ToString()?.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase) ?? false); + + valueList = filteredValueName.Concat(filteredValueList).Distinct().ToList(); + } + + foreach (var valueEntry in valueList.OrderBy(found => found.Key)) + { + var valueName = valueEntry.Key; + if (string.IsNullOrEmpty(valueName)) + { + valueName = "(Default)"; + } + + var registryEntry = new RegistryEntry(key, valueEntry.Key, valueEntry.Value); + + resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry)) + { + Icon = RegistryListPage.RegistryIcon, + Subtitle = GetTruncatedText(GetSubTileForRegistryValue(key, valueEntry), MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight), + Title = GetTruncatedText(valueName, MaxTextLength.MaximumTitleLengthWithThreeSymbols), + MoreCommands = ContextMenuHelper.GetContextMenu(registryEntry).ToArray(), + + // TODO Investigate -->ToolTipData = new ToolTipData(Resources.RegistryValue, GetToolTipTextForRegistryValue(key, valueEntry)), + }); + } + } + catch (Exception exception) + { + var registryEntry = new RegistryEntry(key.Name, exception); + + resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry)) + { + Icon = RegistryListPage.RegistryIcon, + Subtitle = GetTruncatedText(exception.Message, MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight), + Title = GetTruncatedText(key.Name, MaxTextLength.MaximumTitleLengthWithThreeSymbols), + }); + } + + return resultList; + } + + /// + /// Return a truncated name + /// + /// The text to truncate + /// The maximum length of the text + /// (optional) The side of the truncate + /// A truncated text with a maximum length + internal static string GetTruncatedText(string text, in int maxLength, TruncateSide truncateSide = TruncateSide.OnlyFromLeft) + { + if (truncateSide == TruncateSide.OnlyFromLeft) + { + if (text.Length > maxLength) + { + text = QueryHelper.GetKeyWithShortBaseKey(text); + } + + return text.Length > maxLength ? $"...{text[^maxLength..]}" : text; + } + else + { + return text.Length > maxLength ? $"{text[0..maxLength]}..." : text; + } + } + + /// + /// Return the tool-tip text for a registry value + /// + /// The registry key for the tool-tip + /// The value name and value of the registry value + /// A tool-tip text + private static string GetToolTipTextForRegistryValue(RegistryKey key, KeyValuePair valueEntry) + { + return $"{Resources.KeyName} {key.Name}{Environment.NewLine}" + + $"{Resources.Name} {valueEntry.Key}{Environment.NewLine}" + + $"{Resources.Type} {ValueHelper.GetType(key, valueEntry.Key)}{Environment.NewLine}" + + $"{Resources.Value} {ValueHelper.GetValue(key, valueEntry.Key)}"; + } + + /// + /// Return the sub-title text for a registry value + /// + /// The registry key for the sub-title + /// The value name and value of the registry value + /// A sub-title text + private static string GetSubTileForRegistryValue(RegistryKey key, KeyValuePair valueEntry) + { + return $"{Resources.Type} {ValueHelper.GetType(key, valueEntry.Key)}" + + $" - {Resources.Value} {ValueHelper.GetValue(key, valueEntry.Key, 50)}"; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs new file mode 100644 index 0000000000..e0e6eaf951 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Helpers/ValueHelper.cs @@ -0,0 +1,70 @@ +// 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.CmdPal.Ext.Registry.Properties; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Registry.Helpers; + +/// +/// Helper class to easier work with values of a +/// +internal static class ValueHelper +{ + /// + /// Return a human readable value data, of the given value name inside the given + /// + /// The that should contain the value name. + /// The name of the value. + /// The maximum length for the human readable value. + /// A human readable value data. + internal static string GetValue(in RegistryKey key, in string valueName, int maxLength = int.MaxValue) + { + var unformattedValue = key.GetValue(valueName); + + if (unformattedValue == null) + { + throw new InvalidOperationException($"Cannot proceed when {nameof(unformattedValue)} is null."); + } + + var valueData = key.GetValueKind(valueName) switch + { + RegistryValueKind.DWord => $"0x{unformattedValue:X8} ({(uint)(int)unformattedValue})", + RegistryValueKind.QWord => $"0x{unformattedValue:X16} ({(ulong)(long)unformattedValue})", +#pragma warning disable CS8604 // Possible null reference argument. + RegistryValueKind.Binary => (unformattedValue as byte[]).Aggregate(string.Empty, (current, singleByte) => $"{current} {singleByte:X2}"), +#pragma warning restore CS8604 // Possible null reference argument. + _ => $"{unformattedValue}", + }; + + return valueData.Length > maxLength + ? $"{valueData.Substring(0, maxLength)}..." + : valueData; + } + + /// + /// Return the registry type name of a given value name inside a given + /// + /// The that should contain the value name + /// The name of the value + /// A registry type name + internal static object GetType(RegistryKey key, string valueName) + { + return key.GetValueKind(valueName) switch + { + RegistryValueKind.None => Resources.RegistryValueKindNone, + RegistryValueKind.Unknown => Resources.RegistryValueKindUnknown, + RegistryValueKind.String => "REG_SZ", + RegistryValueKind.ExpandString => "REG_EXPAND_SZ", + RegistryValueKind.MultiString => "REG_MULTI_SZ", + RegistryValueKind.Binary => "REG_BINARY", + RegistryValueKind.DWord => "REG_DWORD", + RegistryValueKind.QWord => "REG_QWORD", + _ => throw new ArgumentOutOfRangeException(nameof(valueName)), + }; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj new file mode 100644 index 0000000000..2ff67f859f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj @@ -0,0 +1,44 @@ + + + + Microsoft.CmdPal.Ext.Registry + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.Registry.pri + + + + + + + + + + True + True + Resources.resx + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs new file mode 100644 index 0000000000..34ca4e5d21 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs @@ -0,0 +1,78 @@ +// 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.Registry.Classes; +using Microsoft.CmdPal.Ext.Registry.Helpers; +using Microsoft.CmdPal.Ext.Registry.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Registry; + +internal sealed partial class RegistryListPage : DynamicListPage +{ + public static IconInfo RegistryIcon { get; } = new("\uE74C"); // OEM + + private readonly CommandItem _emptyMessage; + + public RegistryListPage() + { + Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg"); + Name = Title = Resources.Registry_Page_Title; + Id = "com.microsoft.cmdpal.registry"; + _emptyMessage = new CommandItem() + { + Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg"), + Title = Resources.Registry_Key_Not_Found, + Subtitle = SearchText, + }; + EmptyContent = _emptyMessage; + } + + public List Query(string query) + { + if (query is null) + { + return []; + } + + var searchForValueName = QueryHelper.GetQueryParts(query, out var queryKey, out var queryValueName); + + var (baseKeyList, subKey) = RegistryHelper.GetRegistryBaseKey(queryKey); + if (baseKeyList is null) + { + // no base key found + return ResultHelper.GetResultList(RegistryHelper.GetAllBaseKeys()); + } + else if (baseKeyList.Count() == 1) + { + // only one base key was found -> start search for the sub-key + var list = RegistryHelper.SearchForSubKey(baseKeyList.First(), subKey); + + // when only one sub-key was found and a user search for values ("\\") + // show the filtered list of values of one sub-key + return searchForValueName && list.Count == 1 + ? ResultHelper.GetValuesFromKey(list.First().Key, queryValueName) + : ResultHelper.GetResultList(list); + } + else if (baseKeyList.Count() > 1) + { + // more than one base key was found -> show results + return ResultHelper.GetResultList(baseKeyList.Select(found => new RegistryEntry(found))); + } + + return []; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + _emptyMessage.Subtitle = newSearch; + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() => Query(SearchText).ToArray(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..2906e09230 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.Designer.cs @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.Registry.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.Registry.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Copy key name (path). + /// + internal static string CopyKeyNamePath { + get { + return ResourceManager.GetString("CopyKeyNamePath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy value data. + /// + internal static string CopyValueData { + get { + return ResourceManager.GetString("CopyValueData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy value name. + /// + internal static string CopyValueName { + get { + return ResourceManager.GetString("CopyValueName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Key name:. + /// + internal static string KeyName { + get { + return ResourceManager.GetString("KeyName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name:. + /// + internal static string Name { + get { + return ResourceManager.GetString("Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You do not have enough rights to open the Windows registry editor. + /// + internal static string OpenInRegistryEditorAccessExceptionText { + get { + return ResourceManager.GetString("OpenInRegistryEditorAccessExceptionText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error on open registry editor. + /// + internal static string OpenInRegistryEditorAccessExceptionTitle { + get { + return ResourceManager.GetString("OpenInRegistryEditorAccessExceptionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open key in registry editor. + /// + internal static string OpenKeyInRegistryEditor { + get { + return ResourceManager.GetString("OpenKeyInRegistryEditor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navigates inside the Windows Registry. + /// + internal static string PluginDescription { + get { + return ResourceManager.GetString("PluginDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Registry Plugin. + /// + internal static string PluginTitle { + get { + return ResourceManager.GetString("PluginTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Registry key not found. + /// + internal static string Registry_Key_Not_Found { + get { + return ResourceManager.GetString("Registry_Key_Not_Found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Registry. + /// + internal static string Registry_Page_Title { + get { + return ResourceManager.GetString("Registry_Page_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Registry key. + /// + internal static string RegistryKey { + get { + return ResourceManager.GetString("RegistryKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Registry value. + /// + internal static string RegistryValue { + get { + return ResourceManager.GetString("RegistryValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No data type. + /// + internal static string RegistryValueKindNone { + get { + return ResourceManager.GetString("RegistryValueKindNone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported data type. + /// + internal static string RegistryValueKindUnknown { + get { + return ResourceManager.GetString("RegistryValueKindUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subkeys:. + /// + internal static string SubKeys { + get { + return ResourceManager.GetString("SubKeys", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type:. + /// + internal static string Type { + get { + return ResourceManager.GetString("Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value:. + /// + internal static string Value { + get { + return ResourceManager.GetString("Value", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Values:. + /// + internal static string Values { + get { + return ResourceManager.GetString("Values", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.resx new file mode 100644 index 0000000000..f6e3005c68 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.resx @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Registry Plugin + + + Navigates inside the Windows Registry + "this built into Windows the OS. translate accordingly, https://learn.microsoft.com/troubleshoot/windows-server/performance/windows-registry-advanced-users is an example of it translated in German" + + + Copy key name (path) + 'The maximum size of a key name is 255 characters.' + + + Key name: + 'The maximum size of a key name is 255 characters.' + + + Name: + + + Copy value name + + + Open key in registry editor + "registry editor" is the name of the OS built-in application + + + You do not have enough rights to open the Windows registry editor + "registry editor" is the name of the OS built-in application + + + Error on open registry editor + "registry editor" is the name of the OS built-in application + + + Subkeys: + + + Values: + + + Registry key + + + Registry value + + + Type: + See https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types for proper context of how to translate 'type' + + + Value: + See https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types for proper context of how to translate 'value' + + + No data type + + + Unsupported data type + + + Copy value data + + + Windows Registry + + + Registry key not found + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs new file mode 100644 index 0000000000..83b6b1c4a2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Registry; + +public partial class RegistryCommandsProvider : CommandProvider +{ + public RegistryCommandsProvider() + { + Id = "Windows.Registry"; + DisplayName = $"Windows Registry"; + Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg"); + } + + public override ICommandItem[] TopLevelCommands() + { + return [ + new CommandItem(new RegistryListPage()) + { + Title = "Registry", + Subtitle = "Navigate the Windows registry", + } + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs new file mode 100644 index 0000000000..3bc1ec09d7 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs @@ -0,0 +1,258 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.IO; +using Microsoft.CmdPal.Ext.Shell.Helpers; +using Microsoft.CmdPal.Ext.Shell.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell.Commands; + +internal sealed partial class ExecuteItem : InvokableCommand +{ + private readonly SettingsManager _settings; + private readonly RunAsType _runas; + + public string Cmd { get; internal set; } = string.Empty; + + private static readonly char[] Separator = [' ']; + + public ExecuteItem(string cmd, SettingsManager settings, RunAsType type = RunAsType.None) + { + if (type == RunAsType.Administrator) + { + Name = Properties.Resources.cmd_run_as_administrator; + Icon = new IconInfo("\xE7EF"); // Admin Icon + } + else if (type == RunAsType.OtherUser) + { + Name = Properties.Resources.cmd_run_as_user; + Icon = new IconInfo("\xE7EE"); // User Icon + } + else + { + Name = Properties.Resources.generic_run_command; + Icon = new IconInfo("\uE751"); // Return Key Icon + } + + Cmd = cmd; + _settings = settings; + _runas = type; + } + + private static bool ExistInPath(string filename) + { + if (File.Exists(filename)) + { + return true; + } + else + { + var values = Environment.GetEnvironmentVariable("PATH"); + if (values != null) + { + foreach (var path in values.Split(';')) + { + var path1 = Path.Combine(path, filename); + var path2 = Path.Combine(path, filename + ".exe"); + if (File.Exists(path1) || File.Exists(path2)) + { + return true; + } + } + + return false; + } + else + { + return false; + } + } + } + + private void Execute(Func startProcess, ProcessStartInfo info) + { + if (startProcess == null) + { + return; + } + + try + { + startProcess(info); + } + catch (FileNotFoundException e) + { + var name = "Plugin: " + Properties.Resources.cmd_plugin_name; + var message = $"{Properties.Resources.cmd_command_not_found}: {e.Message}"; + + // GH TODO #138 -- show this message once that's wired up + // _context.API.ShowMsg(name, message); + } + catch (Win32Exception e) + { + var name = "Plugin: " + Properties.Resources.cmd_plugin_name; + var message = $"{Properties.Resources.cmd_command_failed}: {e.Message}"; + ExtensionHost.LogMessage(new LogMessage() { Message = name + message }); + + // GH TODO #138 -- show this message once that's wired up + // _context.API.ShowMsg(name, message); + } + } + + public static ProcessStartInfo SetProcessStartInfo(string fileName, string workingDirectory = "", string arguments = "", string verb = "") + { + var info = new ProcessStartInfo + { + FileName = fileName, + WorkingDirectory = workingDirectory, + Arguments = arguments, + Verb = verb, + }; + + return info; + } + + private ProcessStartInfo PrepareProcessStartInfo(string command, RunAsType runAs = RunAsType.None) + { + command = Environment.ExpandEnvironmentVariables(command); + var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + // Set runAsArg + var runAsVerbArg = string.Empty; + if (runAs == RunAsType.OtherUser) + { + runAsVerbArg = "runAsUser"; + } + else if (runAs == RunAsType.Administrator || _settings.RunAsAdministrator) + { + runAsVerbArg = "runAs"; + } + + if (Enum.TryParse(_settings.ShellCommandExecution, out var executionShell)) + { + ProcessStartInfo info; + if (executionShell == ExecutionShell.Cmd) + { + var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause"; + + info = SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.Powershell) + { + var arguments = _settings.LeaveShellOpen + ? $"-NoExit \"{command}\"" + : $"\"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\""; + info = SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.PowerShellSeven) + { + var arguments = _settings.LeaveShellOpen + ? $"-NoExit -C \"{command}\"" + : $"-C \"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\""; + info = SetProcessStartInfo("pwsh.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.WindowsTerminalCmd) + { + var arguments = _settings.LeaveShellOpen ? $"cmd.exe /k \"{command}\"" : $"cmd.exe /c \"{command}\" & pause"; + info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.WindowsTerminalPowerShell) + { + var arguments = _settings.LeaveShellOpen ? $"powershell -NoExit -C \"{command}\"" : $"powershell -C \"{command}\""; + info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.WindowsTerminalPowerShellSeven) + { + var arguments = _settings.LeaveShellOpen ? $"pwsh.exe -NoExit -C \"{command}\"" : $"pwsh.exe -C \"{command}\""; + info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg); + } + else if (executionShell == ExecutionShell.RunCommand) + { + // Open explorer if the path is a file or directory + if (Directory.Exists(command) || File.Exists(command)) + { + info = SetProcessStartInfo("explorer.exe", arguments: command, verb: runAsVerbArg); + } + else + { + var parts = command.Split(Separator, 2); + if (parts.Length == 2) + { + var filename = parts[0]; + if (ExistInPath(filename)) + { + var arguments = parts[1]; + if (_settings.LeaveShellOpen) + { + // Wrap the command in a cmd.exe process + info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{filename} {arguments}\"", runAsVerbArg); + } + else + { + info = SetProcessStartInfo(filename, workingDirectory, arguments, runAsVerbArg); + } + } + else + { + if (_settings.LeaveShellOpen) + { + // Wrap the command in a cmd.exe process + info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg); + } + else + { + info = SetProcessStartInfo(command, verb: runAsVerbArg); + } + } + } + else + { + if (_settings.LeaveShellOpen) + { + // Wrap the command in a cmd.exe process + info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg); + } + else + { + info = SetProcessStartInfo(command, verb: runAsVerbArg); + } + } + } + } + else + { + throw new NotImplementedException(); + } + + info.UseShellExecute = true; + + _settings.AddCmdHistory(command); + + return info; + } + else + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Error extracting setting" }); + throw new NotImplementedException(); + } + } + + public override CommandResult Invoke() + { + try + { + Execute(Process.Start, PrepareProcessStartInfo(Cmd, _runas)); + } + catch + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Error starting the process " }); + } + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/RunAsType.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/RunAsType.cs new file mode 100644 index 0000000000..03b1d3d569 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/RunAsType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Shell.Helpers; + +public enum RunAsType +{ + None, + Administrator, + OtherUser, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..a39e723338 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using Microsoft.CmdPal.Ext.Shell.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "shell"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _choices = + [ + new ChoiceSetSetting.Choice(Resources.find_executable_file_and_run_it, "2"), // idk why but this is how PT Run did it? Maybe ordering matters there + new ChoiceSetSetting.Choice(Resources.run_command_in_command_prompt, "0"), + new ChoiceSetSetting.Choice(Resources.run_command_in_powershell, "1"), + new ChoiceSetSetting.Choice(Resources.run_command_in_powershell_seven, "6"), + new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_cmd, "5"), + new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_powershell, "3"), + new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_powershell_seven, "4"), + ]; + + private readonly ToggleSetting _leaveShellOpen = new( + Namespaced(nameof(LeaveShellOpen)), + Resources.leave_shell_open, + Resources.leave_shell_open, + false); // TODO -- double check default value + + private readonly ChoiceSetSetting _shellCommandExecution = new( + Namespaced(nameof(ShellCommandExecution)), + Resources.shell_command_execution, + Resources.shell_command_execution_description, + _choices); + + public bool LeaveShellOpen => _leaveShellOpen.Value; + + public string ShellCommandExecution => _shellCommandExecution.Value ?? string.Empty; + + public bool RunAsAdministrator { get; set; } + + public Dictionary Count { get; } = []; + + public void AddCmdHistory(string cmdName) => Count[cmdName] = Count.TryGetValue(cmdName, out var currentCount) ? currentCount + 1 : 1; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_leaveShellOpen); + Settings.Add(_shellCommandExecution); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs new file mode 100644 index 0000000000..4ce9076e80 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Shell.Commands; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell.Helpers; + +public class ShellListPageHelpers +{ + private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times); + private readonly SettingsManager _settings; + + public ShellListPageHelpers(SettingsManager settings) + { + _settings = settings; + } + + private ListItem GetCurrentCmd(string cmd) + { + ListItem result = new ListItem(new ExecuteItem(cmd, _settings)) + { + Title = cmd, + Subtitle = Properties.Resources.cmd_plugin_name + ": " + Properties.Resources.cmd_execute_through_shell, + Icon = new IconInfo(string.Empty), + }; + + return result; + } + + private List GetHistoryCmds(string cmd, ListItem result) + { + IEnumerable history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase)) + .OrderByDescending(o => o.Value) + .Select(m => + { + if (m.Key == cmd) + { + // Using CurrentCulture since this is user facing + result.Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value); + return null; + } + + var ret = new ListItem(new ExecuteItem(m.Key, _settings)) + { + Title = m.Key, + + // Using CurrentCulture since this is user facing + Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value), + Icon = new IconInfo("\uE81C"), + }; + return ret; + }).Where(o => o != null).Take(4); + return history.Select(o => o!).ToList(); + } + + public List Query(string query) + { + ArgumentNullException.ThrowIfNull(query); + + List results = new List(); + var cmd = query; + if (string.IsNullOrEmpty(cmd)) + { + results = ResultsFromlHistory(); + } + else + { + var queryCmd = GetCurrentCmd(cmd); + results.Add(queryCmd); + var history = GetHistoryCmds(cmd, queryCmd); + results.AddRange(history); + } + + foreach (var currItem in results) + { + currItem.MoreCommands = LoadContextMenus(currItem).ToArray(); + } + + return results; + } + + public List LoadContextMenus(ListItem listItem) + { + var resultlist = new List + { + new(new ExecuteItem(listItem.Title, _settings, RunAsType.Administrator)), + new(new ExecuteItem(listItem.Title, _settings, RunAsType.OtherUser )), + }; + + return resultlist; + } + + private List ResultsFromlHistory() + { + IEnumerable history = _settings.Count.OrderByDescending(o => o.Value) + .Select(m => new ListItem(new ExecuteItem(m.Key, _settings)) + { + Title = m.Key, + + // Using CurrentCulture since this is user facing + Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value), + Icon = new IconInfo("\uE81C"), + }).Take(5); + + return history.ToList(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj new file mode 100644 index 0000000000..73ad42fe38 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj @@ -0,0 +1,33 @@ + + + + enable + Microsoft.CmdPal.Ext.Shell + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + + + + PreserveNewest + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs new file mode 100644 index 0000000000..4bc5a5a8e6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs @@ -0,0 +1,28 @@ +// 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.Shell.Helpers; +using Microsoft.CmdPal.Ext.Shell.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell.Pages; + +internal sealed partial class ShellListPage : DynamicListPage +{ + private readonly ShellListPageHelpers _helper; + + public ShellListPage(SettingsManager settingsManager) + { + Icon = new IconInfo("\uE756"); + Id = "com.microsoft.cmdpal.shell"; + Name = Resources.cmd_plugin_name; + PlaceholderText = Resources.list_placeholder_text; + _helper = new(settingsManager); + } + + public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0); + + public override IListItem[] GetItems() => [.. _helper.Query(SearchText)]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..2312fa6bd8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs @@ -0,0 +1,279 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.Shell.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.Shell.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Error running the command. + /// + public static string cmd_command_failed { + get { + return ResourceManager.GetString("cmd_command_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command not found. + /// + public static string cmd_command_not_found { + get { + return ResourceManager.GetString("cmd_command_not_found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to execute command through command shell. + /// + public static string cmd_execute_through_shell { + get { + return ResourceManager.GetString("cmd_execute_through_shell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to this command has been executed {0} times. + /// + public static string cmd_has_been_executed_times { + get { + return ResourceManager.GetString("cmd_has_been_executed_times", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Executes commands (e.g. 'ping', 'cmd'). + /// + public static string cmd_plugin_description { + get { + return ResourceManager.GetString("cmd_plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run commands. + /// + public static string cmd_plugin_name { + get { + return ResourceManager.GetString("cmd_plugin_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run as administrator (Ctrl+Shift+Enter). + /// + public static string cmd_run_as_administrator { + get { + return ResourceManager.GetString("cmd_run_as_administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run as different user (Ctrl+Shift+U). + /// + public static string cmd_run_as_user { + get { + return ResourceManager.GetString("cmd_run_as_user", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Find and run the executable file. + /// + public static string find_executable_file_and_run_it { + get { + return ResourceManager.GetString("find_executable_file_and_run_it", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run. + /// + public static string generic_run_command { + get { + return ResourceManager.GetString("generic_run_command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep shell open. + /// + public static string leave_shell_open { + get { + return ResourceManager.GetString("leave_shell_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type the name of a command to run. + /// + public static string list_placeholder_text { + get { + return ResourceManager.GetString("list_placeholder_text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in Command Prompt (cmd.exe). + /// + public static string run_command_in_command_prompt { + get { + return ResourceManager.GetString("run_command_in_command_prompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in PowerShell (PowerShell.exe). + /// + public static string run_command_in_powershell { + get { + return ResourceManager.GetString("run_command_in_powershell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in PowerShell 7 (pwsh.exe). + /// + public static string run_command_in_powershell_seven { + get { + return ResourceManager.GetString("run_command_in_powershell_seven", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in Command Prompt (cmd.exe) using Windows Terminal. + /// + public static string run_command_in_windows_terminal_cmd { + get { + return ResourceManager.GetString("run_command_in_windows_terminal_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in PowerShell (PowerShell.exe) using Windows Terminal. + /// + public static string run_command_in_windows_terminal_powershell { + get { + return ResourceManager.GetString("run_command_in_windows_terminal_powershell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run in PowerShell 7 (pwsh.exe) using Windows Terminal. + /// + public static string run_command_in_windows_terminal_powershell_seven { + get { + return ResourceManager.GetString("run_command_in_windows_terminal_powershell_seven", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Press Enter to continue. + /// + public static string run_plugin_cmd_wait_message { + get { + return ResourceManager.GetString("run_plugin_cmd_wait_message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string settings_page_name { + get { + return ResourceManager.GetString("settings_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run commands. + /// + public static string shell_command_display_title { + get { + return ResourceManager.GetString("shell_command_display_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command execution. + /// + public static string shell_command_execution { + get { + return ResourceManager.GetString("shell_command_execution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entries using the Windows Terminal force the Windows Terminal as the console host regardless of the system settings. + /// + public static string shell_command_execution_description { + get { + return ResourceManager.GetString("shell_command_execution_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run commands. + /// + public static string shell_command_name { + get { + return ResourceManager.GetString("shell_command_name", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx new file mode 100644 index 0000000000..167357821a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Run commands + + + Executes commands (e.g. 'ping', 'cmd') + + + this command has been executed {0} times + + + execute command through command shell + + + Run as administrator (Ctrl+Shift+Enter) + + + Error running the command + + + Command not found + + + Run as different user (Ctrl+Shift+U) + + + Keep shell open + + + Command execution + + + Run in Command Prompt (cmd.exe) + + + Run in PowerShell (PowerShell.exe) + + + Find and run the executable file + + + Run in PowerShell 7 (pwsh.exe) + + + Run in Command Prompt (cmd.exe) using Windows Terminal + + + Run in PowerShell (PowerShell.exe) using Windows Terminal + + + Run in PowerShell 7 (pwsh.exe) using Windows Terminal + + + Press Enter to continue + "Enter" means the Enter key on the keyboard. + + + All entries using the Windows Terminal force the Windows Terminal as the console host regardless of the system settings + + + Run commands + + + Run + + + Settings + + + Type the name of a command to run + + + Run commands + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs new file mode 100644 index 0000000000..9e98e036d4 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs @@ -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.CmdPal.Ext.Shell.Helpers; +using Microsoft.CmdPal.Ext.Shell.Pages; +using Microsoft.CmdPal.Ext.Shell.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell; + +public partial class ShellCommandsProvider : CommandProvider +{ + private readonly CommandItem _shellPageItem; + private readonly SettingsManager _settingsManager = new(); + private readonly FallbackCommandItem _fallbackItem; + + public ShellCommandsProvider() + { + Id = "Run"; + DisplayName = Resources.cmd_plugin_name; + Icon = Icons.RunV2; + Settings = _settingsManager.Settings; + + _fallbackItem = new FallbackExecuteItem(_settingsManager); + + _shellPageItem = new CommandItem(new ShellListPage(_settingsManager)) + { + Icon = Icons.RunV2, + Title = Resources.shell_command_name, + Subtitle = Resources.cmd_plugin_description, + MoreCommands = [ + new CommandContextItem(Settings.SettingsPage), + ], + }; + } + + public override ICommandItem[] TopLevelCommands() => [_shellPageItem]; + + public override IFallbackCommandItem[]? FallbackCommands() => [_fallbackItem]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.dark.png new file mode 100644 index 0000000000..01da9ffd2f Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.light.png new file mode 100644 index 0000000000..8d320d126c Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/firmwareSettings.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.dark.png new file mode 100644 index 0000000000..23eb293021 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.light.png new file mode 100644 index 0000000000..a1e8b63585 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/logoff.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.dark.png new file mode 100644 index 0000000000..bd0e3d4b5a Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.light.png new file mode 100644 index 0000000000..d99184d1a6 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Assets/sleep.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinCommand.cs new file mode 100644 index 0000000000..932e421e42 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinCommand.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.System.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Shell; + +public sealed partial class EmptyRecycleBinCommand : InvokableCommand +{ + public EmptyRecycleBinCommand(bool settingEmptyRBSuccesMsg) + { + _settingEmptyRBSuccesMsg = settingEmptyRBSuccesMsg; + } + + public override CommandResult Invoke() + { + Task.Run(() => ResultHelper.EmptyRecycleBinTask(_settingEmptyRBSuccesMsg)); + + return CommandResult.Dismiss(); + } + + private bool _settingEmptyRBSuccesMsg; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinConfirmation.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinConfirmation.cs new file mode 100644 index 0000000000..c15247d8cb --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/EmptyRecycleBinConfirmation.cs @@ -0,0 +1,38 @@ +// 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.Shell; +using Microsoft.CmdPal.Ext.System.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System; + +public sealed partial class EmptyRecycleBinConfirmation : InvokableCommand +{ + public EmptyRecycleBinConfirmation(bool settingEmptyRBSuccesMsg) + { + Name = Resources.Microsoft_plugin_command_name_empty; + _settingEmptyRBSuccesMsg = settingEmptyRBSuccesMsg; + } + + public override CommandResult Invoke() + { + if (ResultHelper.ExecutingEmptyRecycleBinTask) + { + return CommandResult.ShowToast(new ToastArgs() { Message = Resources.Microsoft_plugin_sys_RecycleBin_EmptyTaskRunning }); + } + + var confirmArgs = new ConfirmationArgs() + { + Title = Resources.Microsoft_plugin_sys_confirmation, + Description = Resources.EmptyRecycleBin_ConfirmationDialog_Description, + PrimaryCommand = new EmptyRecycleBinCommand(_settingEmptyRBSuccesMsg), + IsPrimaryCommandCritical = true, + }; + + return CommandResult.Confirm(confirmArgs); + } + + private bool _settingEmptyRBSuccesMsg; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommand.cs new file mode 100644 index 0000000000..d890ed39b6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommand.cs @@ -0,0 +1,24 @@ +// 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.Toolkit; + +namespace Microsoft.CmdPal.Ext.System; + +public sealed partial class ExecuteCommand : InvokableCommand +{ + public ExecuteCommand(Action command) + { + _command = command; + } + + public override CommandResult Invoke() + { + _command(); + return CommandResult.Dismiss(); + } + + private Action _command; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommandConfirmation.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommandConfirmation.cs new file mode 100644 index 0000000000..a82935fa82 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/ExecuteCommandConfirmation.cs @@ -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 System; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System; + +public sealed partial class ExecuteCommandConfirmation : InvokableCommand +{ + public ExecuteCommandConfirmation(string name, bool confirm, string confirmationMessage, Action command) + { + Name = name; + _command = command; + _confirm = confirm; + _confirmationMessage = confirmationMessage; + } + + public override CommandResult Invoke() + { + if (_confirm) + { + var confirmationArgs = new ConfirmationArgs + { + Title = Resources.Microsoft_plugin_sys_confirmation, + Description = _confirmationMessage, + PrimaryCommand = new ExecuteCommand(_command), + IsPrimaryCommandCritical = true, + }; + + return CommandResult.Confirm(confirmationArgs); + } + + _command(); + return CommandResult.Dismiss(); + } + + private bool _confirm; + private string _confirmationMessage; + private Action _command; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs new file mode 100644 index 0000000000..039e7c55b3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.NetworkInformation; +using System.Text; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +/// +/// This class holds all available results +/// +internal static class Commands +{ + internal const int EWXLOGOFF = 0x00000000; + internal const int EWXSHUTDOWN = 0x00000001; + internal const int EWXREBOOT = 0x00000002; + internal const int EWXFORCE = 0x00000004; + internal const int EWXPOWEROFF = 0x00000008; + internal const int EWXFORCEIFHUNG = 0x00000010; + + // Cache for network interface information to save query time + private const int UpdateCacheIntervalSeconds = 5; + private static List networkPropertiesCache = new List(); + private static DateTime timeOfLastNetworkQuery; + + /// + /// Returns a list with all system command results + /// + /// Value indicating if the system is booted in uefi mode + /// Value indicating if we should show two results for Recycle Bin. + /// A value indicating if the user should confirm the system commands + /// Show a success message after empty Recycle Bin. + /// A list of all results + public static List GetSystemCommands(bool isUefi, bool splitRecycleBinResults, bool confirmCommands, bool emptyRBSuccessMessage) + { + var results = new List(); + results.AddRange(new[] + { + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0"))) + { + Title = Resources.Microsoft_plugin_sys_shutdown_computer, + Subtitle = Resources.Microsoft_plugin_sys_shutdown_computer_description, + Icon = Icons.ShutdownIcon, + }, + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0"))) + { + Title = Resources.Microsoft_plugin_sys_restart_computer, + Subtitle = Resources.Microsoft_plugin_sys_restart_computer_description, + Icon = Icons.RestartIcon, + }, + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0))) + { + Title = Resources.Microsoft_plugin_sys_sign_out, + Subtitle = Resources.Microsoft_plugin_sys_sign_out_description, + Icon = Icons.LogoffIcon, + }, + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation())) + { + Title = Resources.Microsoft_plugin_sys_lock, + Subtitle = Resources.Microsoft_plugin_sys_lock_description, + Icon = Icons.LockIcon, + }, + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true))) + { + Title = Resources.Microsoft_plugin_sys_sleep, + Subtitle = Resources.Microsoft_plugin_sys_sleep_description, + Icon = Icons.SleepIcon, + }, + new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true))) + { + Title = Resources.Microsoft_plugin_sys_hibernate, + Subtitle = Resources.Microsoft_plugin_sys_hibernate_description, + Icon = Icons.SleepIcon, // Icon change needed + }, + }); + + // Show Recycle Bin results based on setting. + if (splitRecycleBinResults) + { + results.AddRange(new[] + { + new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_empty, "explorer.exe", "shell:RecycleBinFolder")) + { + Title = Resources.Microsoft_plugin_sys_RecycleBinOpen, + Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description, + Icon = Icons.RecycleBinIcon, + }, + new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage)) + { + Title = Resources.Microsoft_plugin_sys_RecycleBinEmptyResult, + Subtitle = Resources.Microsoft_plugin_sys_RecycleBinEmpty_description, + Icon = Icons.RecycleBinIcon, + }, + }); + } + else + { + results.Add( + new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_empty, "explorer.exe", "shell:RecycleBinFolder")) + { + Title = Resources.Microsoft_plugin_sys_RecycleBin, + Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description, + Icon = Icons.RecycleBinIcon, + }); + } + + // UEFI command/result. It is only available on systems booted in UEFI mode. + if (isUefi) + { + results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_reboot, confirmCommands, Resources.Microsoft_plugin_sys_uefi_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/r /fw /t 0", null, OpenInShellHelper.ShellRunAsType.Administrator))) + { + Title = Resources.Microsoft_plugin_sys_uefi, + Subtitle = Resources.Microsoft_plugin_sys_uefi_description, + Icon = Icons.FirmwareSettingsIcon, + }); + } + + return results; + } + + /// + /// Returns a list of all ip and mac results + /// + /// The tSettingsManager instance + /// The list of available results + public static List GetNetworkConnectionResults(SettingsManager manager) + { + var results = new List(); + + // We update the cache only if the last query is older than 'updateCacheIntervalSeconds' seconds + DateTime timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery; + timeOfLastNetworkQuery = DateTime.Now; // Set time of last query to this query + if ((timeOfLastNetworkQuery - timeOfLastNetworkQueryBefore).TotalSeconds >= UpdateCacheIntervalSeconds) + { + networkPropertiesCache = NetworkConnectionProperties.GetList(); + } + + CompositeFormat sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description); + CompositeFormat sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description); + var hideDisconnectedNetworkInfo = manager.HideDisconnectedNetworkInfo; + + foreach (NetworkConnectionProperties intInfo in networkPropertiesCache) + { + if (hideDisconnectedNetworkInfo) + { + if (intInfo.State != OperationalStatus.Up) + { + continue; + } + } + + if (!string.IsNullOrEmpty(intInfo.IPv4)) + { + results.Add(new ListItem(new CopyTextCommand(intInfo.GetConnectionDetails())) + { + Title = intInfo.IPv4, + Subtitle = string.Format(CultureInfo.InvariantCulture, sysIpv4DescriptionCompositeFormate, intInfo.ConnectionName), + Icon = Icons.NetworkAdapterIcon, + Details = new Details() { Title = Resources.Microsoft_plugin_ext_connection_details, Body = intInfo.GetConnectionDetails() }, + }); + } + + if (!string.IsNullOrEmpty(intInfo.IPv6Primary)) + { + results.Add(new ListItem(new CopyTextCommand(intInfo.GetConnectionDetails())) + { + Title = intInfo.IPv6Primary, + Subtitle = string.Format(CultureInfo.InvariantCulture, sysIpv4DescriptionCompositeFormate, intInfo.ConnectionName), + Icon = Icons.NetworkAdapterIcon, + Details = new Details() { Title = Resources.Microsoft_plugin_ext_connection_details, Body = intInfo.GetConnectionDetails() }, + }); + } + + if (!string.IsNullOrEmpty(intInfo.PhysicalAddress)) + { + results.Add(new ListItem(new CopyTextCommand(intInfo.GetAdapterDetails())) + { + Title = intInfo.PhysicalAddress, + Subtitle = string.Format(CultureInfo.InvariantCulture, sysMacDescriptionCompositeFormate, intInfo.Adapter, intInfo.ConnectionName), + Icon = Icons.NetworkAdapterIcon, + Details = new Details() { Title = Resources.Microsoft_plugin_ext_connection_details, Body = intInfo.GetConnectionDetails() }, + }); + } + } + + return results; + } + + public static List GetAllCommands(SettingsManager manager) + { + var list = new List(); + var listLock = new object(); + + // Network (ip and mac) results are slow with many network cards and returned delayed. + // On global queries the first word/part has to be 'ip', 'mac' or 'address' for network results + var networkConnectionResults = Commands.GetNetworkConnectionResults(manager); + + var isBootedInUefiMode = Win32Helpers.GetSystemFirmwareType() == FirmwareType.Uefi; + + var separateEmptyRB = manager.ShowSeparateResultForEmptyRecycleBin; + var confirmSystemCommands = manager.ShowDialogToConfirmCommand; + var showSuccessOnEmptyRB = manager.ShowSuccessMessageAfterEmptyingRecycleBin; + + // normal system commands are fast and can be returned immediately + var systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, separateEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB); + list.AddRange(systemCommands); + list.AddRange(networkConnectionResults); + + return list; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs new file mode 100644 index 0000000000..71988474ee --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +public static partial class Icons +{ + public static IconInfo FirmwareSettingsIcon { get; } = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.System\\Assets\\logoff.light.png", "Microsoft.CmdPal.Ext.System\\Assets\\logoff.dark.png"); + + public static IconInfo LockIcon { get; } = new IconInfo("\uE72E"); + + public static IconInfo LogoffIcon { get; } = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.System\\Assets\\logoff.light.png", "Microsoft.CmdPal.Ext.System\\Assets\\logoff.dark.png"); + + public static IconInfo NetworkAdapterIcon { get; } = new IconInfo("\uEDA3"); + + public static IconInfo RecycleBinIcon { get; } = new IconInfo("\uE74D"); + + public static IconInfo RestartIcon { get; } = new IconInfo("\uE777"); + + public static IconInfo ShutdownIcon { get; } = new IconInfo("\uE7E8"); + + public static IconInfo SleepIcon { get; } = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.System\\Assets\\sleep.light.png", "Microsoft.CmdPal.Ext.System\\Assets\\sleep.dark.png"); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/MessageBoxHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/MessageBoxHelper.cs new file mode 100644 index 0000000000..62931adee3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/MessageBoxHelper.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +public sealed partial class MessageBoxHelper +{ + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int MessageBox(IntPtr hWnd, string text, string caption, int type); + + public static MessageBoxResult Show(string text, string caption, IconType iconType, MessageBoxType type) + { + return (MessageBoxResult)MessageBox(IntPtr.Zero, text, caption, (int)type | (int)iconType); + } + + public enum IconType + { + Error = 0x00000010, + Help = 0x00000020, + Warning = 0x00000030, + Info = 0x00000040, + } + + public enum MessageBoxType + { + OK = 0x00000000, + } + + public enum MessageBoxResult + { + OK = 1, + Cancel = 2, + Abort = 3, + Retry = 4, + Ignore = 5, + Yes = 6, + No = 7, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Native.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Native.cs new file mode 100644 index 0000000000..3daf2a4be4 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Native.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")] +public sealed class Native +{ + public enum HRESULT : uint + { + /// + /// Operation successful. + /// + S_OK = 0x00000000, + + /// + /// Operation successful. (negative condition/no operation) + /// + S_FALSE = 0x00000001, + + /// + /// Not implemented. + /// + E_NOTIMPL = 0x80004001, + + /// + /// No such interface supported. + /// + E_NOINTERFACE = 0x80004002, + + /// + /// Pointer that is not valid. + /// + E_POINTER = 0x80004003, + + /// + /// Operation aborted. + /// + E_ABORT = 0x80004004, + + /// + /// Unspecified failure. + /// + E_FAIL = 0x80004005, + + /// + /// Unexpected failure. + /// + E_UNEXPECTED = 0x8000FFFF, + + /// + /// General access denied error. + /// + E_ACCESSDENIED = 0x80070005, + + /// + /// Handle that is not valid. + /// + E_HANDLE = 0x80070006, + + /// + /// Failed to allocate necessary memory. + /// + E_OUTOFMEMORY = 0x8007000E, + + /// + /// One or more arguments are not valid. + /// + E_INVALIDARG = 0x80070057, + + /// + /// The operation was canceled by the user. (Error source 7 means Win32.) + /// + /// + /// + E_CANCELLED = 0x800704C7, + } + + public static class ShellItemTypeConstants + { + /// + /// Guid for type IShellItem. + /// + public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe"); + + /// + /// Guid for type IShellItem2. + /// + public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93"); + } + + /// + /// The following are ShellItem DisplayName types. + /// + [Flags] + public enum SIGDN : uint + { + NORMALDISPLAY = 0, + PARENTRELATIVEPARSING = 0x80018001, + PARENTRELATIVEFORADDRESSBAR = 0x8001c001, + DESKTOPABSOLUTEPARSING = 0x80028000, + PARENTRELATIVEEDITING = 0x80031001, + DESKTOPABSOLUTEEDITING = 0x8004c000, + FILESYSPATH = 0x80058000, + URL = 0x80068000, + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] + public interface IShellItem + { + void BindToHandler( + nint pbc, + [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out nint ppv); + + void GetParent(out IShellItem ppsi); + + void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + + void Compare(IShellItem psi, uint hint, out int piOrder); + } + + /// + /// see all STGM values + /// + [Flags] + public enum STGM : long + { + READ = 0x00000000L, + WRITE = 0x00000001L, + READWRITE = 0x00000002L, + CREATE = 0x00001000L, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs new file mode 100644 index 0000000000..e620ab330b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; + +#pragma warning disable SA1649, CA1051, CA1707, CA1028, CA1714, CA1069, SA1402 + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")] +public static class NativeMethods +{ + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetFirmwareType(ref FirmwareType FirmwareType); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ExitWindowsEx(uint uFlags, uint dwReason); + + [DllImport("user32")] + public static extern void LockWorkStation(); + + [DllImport("Powrprof.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); + + [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] + public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags); +} + +public enum HRESULT : uint +{ + /// + /// Operation successful. + /// + S_OK = 0x00000000, + + /// + /// Operation successful. (negative condition/no operation) + /// + S_FALSE = 0x00000001, + + /// + /// Not implemented. + /// + E_NOTIMPL = 0x80004001, + + /// + /// No such interface supported. + /// + E_NOINTERFACE = 0x80004002, + + /// + /// Pointer that is not valid. + /// + E_POINTER = 0x80004003, + + /// + /// Operation aborted. + /// + E_ABORT = 0x80004004, + + /// + /// Unspecified failure. + /// + E_FAIL = 0x80004005, + + /// + /// Unexpected failure. + /// + E_UNEXPECTED = 0x8000FFFF, + + /// + /// General access denied error. + /// + E_ACCESSDENIED = 0x80070005, + + /// + /// Handle that is not valid. + /// + E_HANDLE = 0x80070006, + + /// + /// Failed to allocate necessary memory. + /// + E_OUTOFMEMORY = 0x8007000E, + + /// + /// One or more arguments are not valid. + /// + E_INVALIDARG = 0x80070057, + + /// + /// The operation was canceled by the user. (Error source 7 means Win32.) + /// + /// + /// + E_CANCELLED = 0x800704C7, +} + +/// +/// see learn.microsoft.com +/// +public enum FirmwareType +{ + Unknown = 0, + Bios = 1, + Uefi = 2, + Max = 3, +} + +public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs new file mode 100644 index 0000000000..ce050e95fd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs @@ -0,0 +1,320 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +/// +/// This class represents the information for a network connection/interface +/// +internal sealed class NetworkConnectionProperties +{ + /// + /// Gets the name of the adapter + /// + internal string Adapter { get; private set; } + + /// + /// Gets the physical address (MAC) of the adapter + /// + internal string PhysicalAddress { get; private set; } + + /// + /// Gets a value indicating the interface type + /// + internal NetworkInterfaceType Type { get; private set; } + + /// + /// Gets the speed of the adapter as unformatted value (Static information form the adapter device) + /// + internal long Speed { get; private set; } + + /// + /// Gets a value indicating the operational state of the adapter + /// + internal OperationalStatus State { get; private set; } + + /// + /// Gets the name of the network connection + /// + internal string ConnectionName { get; private set; } = string.Empty; + + /// + /// Gets a string with the suffix of the connection + /// + internal string Suffix { get; private set; } = string.Empty; + + /// + /// Gets the IPv4 address + /// + internal string IPv4 { get; private set; } = string.Empty; + + /// + /// Gets the IPv4 subnet mask + /// + internal string IPv4Mask { get; private set; } = string.Empty; + + /// + /// Gets the primarily used IPv6 address + /// + internal string IPv6Primary { get; private set; } = string.Empty; + + /// + /// Gets the global IPv6 address + /// + internal string IPv6Global { get; private set; } = string.Empty; + + /// + /// Gets the temporary IPv6 address + /// + internal string IPv6Temporary { get; private set; } = string.Empty; + + /// + /// Gets the link local IPv6 address + /// + internal string IPv6LinkLocal { get; private set; } = string.Empty; + + /// + /// Gets the site local IPv6 address + /// + internal string IPv6SiteLocal { get; private set; } = string.Empty; + + /// + /// Gets the unique local IPv6 address + /// + internal string IPv6UniqueLocal { get; private set; } = string.Empty; + + /// + /// Gets the list of gateway IPs as string + /// + internal List Gateways { get; private set; } = new List(); + + /// + /// Gets the list of DHCP server IPs as string + /// + internal IPAddressCollection? DhcpServers { get; private set; } + + /// + /// Gets the list of DNS server IPs as string + /// + internal IPAddressCollection? DnsServers { get; private set; } + + /// + /// Gets the list of WINS server IPs as string + /// + internal IPAddressCollection? WinsServers { get; private set; } + + private static readonly CompositeFormat MicrosoftPluginSysGbps = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_Gbps); + private static readonly CompositeFormat MicrosoftPluginSysMbps = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_Mbps); + + /// + /// Initializes a new instance of the class. + /// This private constructor is used when we crete the list of adapter (properties) by calling . + /// + /// Network interface of the connection + private NetworkConnectionProperties(NetworkInterface networkInterface) + { + // Setting adapter properties + Adapter = networkInterface.Description; + PhysicalAddress = networkInterface.GetPhysicalAddress().ToString(); + Type = networkInterface.NetworkInterfaceType; + Speed = networkInterface.Speed; + State = networkInterface.OperationalStatus; + + // Connection properties + ConnectionName = networkInterface.Name; + if (State == OperationalStatus.Up) + { + Suffix = networkInterface.GetIPProperties().DnsSuffix; + SetIpProperties(networkInterface.GetIPProperties()); + } + } + + /// + /// Creates a list with all network adapters and their properties + /// + /// List containing all network adapters + internal static List GetList() + { + var interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(x => x.NetworkInterfaceType != NetworkInterfaceType.Loopback && x.GetPhysicalAddress() != null) + .Select(i => new NetworkConnectionProperties(i)) + .OrderByDescending(i => i.IPv4) // list IPv4 first + .ThenBy(i => i.IPv6Primary) // then IPv6 + .ToList(); + return interfaces; + } + + /// + /// Gets a formatted string with the adapter details + /// + /// String with the details + internal string GetAdapterDetails() + { + return $"{Resources.Microsoft_plugin_sys_AdapterName}: {Adapter}" + + $"\n{Resources.Microsoft_plugin_sys_PhysicalAddress}: {PhysicalAddress}" + + $"\n{Resources.Microsoft_plugin_sys_Speed}: {GetFormattedSpeedValue(Speed)}" + + $"\n{Resources.Microsoft_plugin_sys_Type}: {GetAdapterTypeAsString(Type)}" + + $"\n{Resources.Microsoft_plugin_sys_State}: " + (State == OperationalStatus.Up ? Resources.Microsoft_plugin_sys_Connected : Resources.Microsoft_plugin_sys_Disconnected) + + $"\n{Resources.Microsoft_plugin_sys_ConnectionName}: {ConnectionName}"; + } + + /// + /// Returns a formatted string with the connection details + /// + /// String with the details + internal string GetConnectionDetails() + { + return $"{Resources.Microsoft_plugin_sys_ConnectionName}: {ConnectionName}" + + $"\n{Resources.Microsoft_plugin_sys_State}: " + (State == OperationalStatus.Up ? Resources.Microsoft_plugin_sys_Connected : Resources.Microsoft_plugin_sys_Disconnected) + + $"\n{Resources.Microsoft_plugin_sys_Type}: {GetAdapterTypeAsString(Type)}" + + $"\n{Resources.Microsoft_plugin_sys_Suffix}: {Suffix}" + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip4Address}: ", IPv4) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip4SubnetMask}: ", IPv4Mask) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip6Address}:\n\t", IPv6Global) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip6Temp}:\n\t", IPv6Temporary) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip6Link}:\n\t", IPv6LinkLocal) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip6Site}:\n\t", IPv6SiteLocal) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Ip6Unique}:\n\t", IPv6UniqueLocal) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Gateways}:\n\t", Gateways) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Dhcp}:\n\t", DhcpServers == null ? string.Empty : DhcpServers) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Dns}:\n\t", DnsServers == null ? string.Empty : DnsServers) + + CreateIpInfoForDetailsText($"{Resources.Microsoft_plugin_sys_Wins}:\n\t", WinsServers == null ? string.Empty : WinsServers) + + $"\n\n{Resources.Microsoft_plugin_sys_AdapterName}: {Adapter}" + + $"\n{Resources.Microsoft_plugin_sys_PhysicalAddress}: {PhysicalAddress}" + + $"\n{Resources.Microsoft_plugin_sys_Speed}: {GetFormattedSpeedValue(Speed)}"; + } + + /// + /// Set the ip address properties of the instance. + /// + /// Element of the type . + private void SetIpProperties(IPInterfaceProperties properties) + { + DateTime t = DateTime.Now; + + UnicastIPAddressInformationCollection ipList = properties.UnicastAddresses; + GatewayIPAddressInformationCollection gwList = properties.GatewayAddresses; + DhcpServers = properties.DhcpServerAddresses; + DnsServers = properties.DnsAddresses; + WinsServers = properties.WinsServersAddresses; + + for (var i = 0; i < ipList.Count; i++) + { + IPAddress ip = ipList[i].Address; + + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + IPv4 = ip.ToString(); + IPv4Mask = ipList[i].IPv4Mask.ToString(); + } + else if (ip.AddressFamily == AddressFamily.InterNetworkV6) + { + if (string.IsNullOrEmpty(IPv6Primary)) + { + IPv6Primary = ip.ToString(); + } + + if (ip.IsIPv6LinkLocal) + { + IPv6LinkLocal = ip.ToString(); + } + else if (ip.IsIPv6SiteLocal) + { + IPv6SiteLocal = ip.ToString(); + } + else if (ip.IsIPv6UniqueLocal) + { + IPv6UniqueLocal = ip.ToString(); + } + else if (ipList[i].SuffixOrigin == SuffixOrigin.Random) + { + IPv6Temporary = ip.ToString(); + } + else + { + IPv6Global = ip.ToString(); + } + } + } + + for (var i = 0; i < gwList.Count; i++) + { + Gateways.Add(gwList[i].Address); + } + + Debug.Print($"time for getting ips: {DateTime.Now - t}"); + } + + /// + /// Gets the interface type as string + /// + /// The type to convert + /// A string indicating the interface type + private string GetAdapterTypeAsString(NetworkInterfaceType type) + { + switch (type) + { + case NetworkInterfaceType.Wman: + case NetworkInterfaceType.Wwanpp: + case NetworkInterfaceType.Wwanpp2: + return Resources.Microsoft_plugin_sys_MobileBroadband; + case NetworkInterfaceType.Wireless80211: + return Resources.Microsoft_plugin_sys_WirelessLan; + case NetworkInterfaceType.Loopback: + return Resources.Microsoft_plugin_sys_Loopback; + case NetworkInterfaceType.Tunnel: + return Resources.Microsoft_plugin_sys_TunnelConnection; + case NetworkInterfaceType.Unknown: + return Resources.Microsoft_plugin_sys_Unknown; + default: + return Resources.Microsoft_plugin_sys_Cable; + } + } + + /// + /// Gets the speed as formatted text value + /// + /// The adapter speed as . + /// A formatted string like `100 MB/s` + private static string GetFormattedSpeedValue(long speed) + { + return (speed >= 1000000000) ? string.Format(CultureInfo.InvariantCulture, MicrosoftPluginSysGbps, speed / 1000000000) : string.Format(CultureInfo.InvariantCulture, MicrosoftPluginSysMbps, speed / 1000000); + } + + /// + /// Returns IP info or an empty string + /// + /// Descriptive header for the information. + /// IP value as or . + /// Formatted string or an empty string. + /// If the parameter is not of the type or . + private static string CreateIpInfoForDetailsText(string title, dynamic property) + { + switch (property) + { + case string: + return $"\n{title}{property}"; + case List listString: + return listString.Count == 0 ? string.Empty : $"\n{title}{string.Join("\n\t", property)}"; + case List listIP: + return listIP.Count == 0 ? string.Empty : $"\n{title}{string.Join("\n\t", property)}"; + case IPAddressCollection collectionIP: + return collectionIP.Count == 0 ? string.Empty : $"\n{title}{string.Join("\n\t", property)}"; + case null: + return string.Empty; + default: + throw new ArgumentException($"'{property}' is not of type 'string', 'List', 'List' or 'IPAddressCollection'.", nameof(property)); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/OpenInShellHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/OpenInShellHelper.cs new file mode 100644 index 0000000000..67053ffcf2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/OpenInShellHelper.cs @@ -0,0 +1,49 @@ +// 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.Diagnostics; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +public static partial class OpenInShellHelper +{ + public static bool OpenInShell(string path, string? arguments = null, string? workingDir = null, ShellRunAsType runAs = ShellRunAsType.None, bool runWithHiddenWindow = false) + { + using var process = new Process(); + process.StartInfo.FileName = path; + process.StartInfo.WorkingDirectory = string.IsNullOrWhiteSpace(workingDir) ? string.Empty : workingDir; + process.StartInfo.Arguments = string.IsNullOrWhiteSpace(arguments) ? string.Empty : arguments; + process.StartInfo.WindowStyle = runWithHiddenWindow ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal; + process.StartInfo.UseShellExecute = true; + + if (runAs == ShellRunAsType.Administrator) + { + process.StartInfo.Verb = "RunAs"; + } + else if (runAs == ShellRunAsType.OtherUser) + { + process.StartInfo.Verb = "RunAsUser"; + } + + try + { + process.Start(); + return true; + } + catch (Win32Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Unable to open {path}: {ex.Message}" }); + return false; + } + } + + public enum ShellRunAsType + { + None, + Administrator, + OtherUser, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/ResultHelper.cs new file mode 100644 index 0000000000..accecfed8c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/ResultHelper.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.System; +using static Microsoft.CmdPal.Ext.System.Helpers.MessageBoxHelper; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +internal static class ResultHelper +{ + public static bool ExecutingEmptyRecycleBinTask { get; set; } + + /// + /// Method to process the empty recycle bin command in a separate task + /// + public static void EmptyRecycleBinTask(bool settingEmptyRBSuccesMsg) + { + ExecutingEmptyRecycleBinTask = true; + + // https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shemptyrecyclebina/ + // http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html/ + // If the recycle bin is already empty, it will return -2147418113 (0x8000FFFF (E_UNEXPECTED)) + // If the user canceled the deletion task it will return 2147943623 (0x800704C7 (E_CANCELLED - The operation was canceled by the user.)) + // On success it will return 0 (S_OK) + var result = NativeMethods.SHEmptyRecycleBin(IntPtr.Zero, 0); + if (result == (uint)HRESULT.E_UNEXPECTED) + { + _ = MessageBoxHelper.Show(Resources.Microsoft_plugin_sys_RecycleBin_IsEmpty, "Plugin: " + Resources.Microsoft_plugin_sys_plugin_name, IconType.Info, MessageBoxType.OK); + } + else if (result != (uint)HRESULT.S_OK && result != (uint)HRESULT.E_CANCELLED) + { + var errorDesc = Win32Helpers.MessageFromHResult((int)result); + var name = "Plugin: " + Resources.Microsoft_plugin_sys_plugin_name; + var message = $"{Resources.Microsoft_plugin_sys_RecycleBin_ErrorMsg} {errorDesc}"; + + ExtensionHost.LogMessage(new LogMessage() { Message = message + " - Please refer to https://msdn.microsoft.com/library/windows/desktop/aa378137 for more information." }); + + _ = MessageBoxHelper.Show(message, name, IconType.Error, MessageBoxType.OK); + } + + if (result == (uint)HRESULT.S_OK && settingEmptyRBSuccesMsg) + { + _ = MessageBoxHelper.Show(Resources.Microsoft_plugin_sys_RecycleBin_EmptySuccessMessage, "Plugin: " + Resources.Microsoft_plugin_sys_plugin_name, IconType.Info, MessageBoxType.OK); + } + + ExecutingEmptyRecycleBinTask = false; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..7f43027156 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SettingsManager.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "system"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private readonly ToggleSetting _showDialogToConfirmCommand = new( + Namespaced(nameof(ShowDialogToConfirmCommand)), + Resources.confirm_system_commands, + Resources.confirm_system_commands, + false); // TODO -- double check default value + + private readonly ToggleSetting _showSuccessMessageAfterEmptyingRecycleBin = new( + Namespaced(nameof(ShowSuccessMessageAfterEmptyingRecycleBin)), + Resources.Microsoft_plugin_sys_RecycleBin_ShowEmptySuccessMessage, + Resources.Microsoft_plugin_sys_RecycleBin_ShowEmptySuccessMessage, + false); // TODO -- double check default value + + private readonly ToggleSetting _showSeparateResultForEmptyRecycleBin = new( + Namespaced(nameof(ShowSeparateResultForEmptyRecycleBin)), + Resources.Microsoft_plugin_sys_RecycleBin_ShowEmptySeparate, + Resources.Microsoft_plugin_sys_RecycleBin_ShowEmptySeparate, + true); // TODO -- double check default value + + private readonly ToggleSetting _hideDisconnectedNetworkInfo = new( + Namespaced(nameof(HideDisconnectedNetworkInfo)), + Resources.Microsoft_plugin_ext_settings_hideDisconnectedNetworkInfo, + Resources.Microsoft_plugin_ext_settings_hideDisconnectedNetworkInfo, + true); // TODO -- double check default value + + public bool ShowDialogToConfirmCommand => _showDialogToConfirmCommand.Value; + + public bool ShowSuccessMessageAfterEmptyingRecycleBin => _showSuccessMessageAfterEmptyingRecycleBin.Value; + + public bool ShowSeparateResultForEmptyRecycleBin => _showSeparateResultForEmptyRecycleBin.Value; + + public bool HideDisconnectedNetworkInfo => _hideDisconnectedNetworkInfo.Value; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_showDialogToConfirmCommand); + Settings.Add(_showSuccessMessageAfterEmptyingRecycleBin); + Settings.Add(_showSeparateResultForEmptyRecycleBin); + Settings.Add(_hideDisconnectedNetworkInfo); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SystemPluginContext.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SystemPluginContext.cs new file mode 100644 index 0000000000..46ca71e77d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/SystemPluginContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +internal sealed class SystemPluginContext +{ + /// + /// Gets or sets the type of the result + /// + public ResultContextType Type { get; set; } + + /// + /// Gets or sets the context data for the command/results + /// + public string Data { get; set; } = string.Empty; + + /// + /// Gets or sets an additional result name for searching + /// + public string SearchTag { get; set; } = string.Empty; +} + +internal enum ResultContextType +{ + Command, // Reserved for later usage + NetworkAdapterInfo, + RecycleBinCommand, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Win32Helpers.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Win32Helpers.cs new file mode 100644 index 0000000000..c4c447da8d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Helpers/Win32Helpers.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +public static class Win32Helpers +{ + /// + /// Detects the type of system firmware which is equal to the boot type by calling the method . + /// + /// Firmware type like Uefi or Bios. + public static FirmwareType GetSystemFirmwareType() + { + FirmwareType firmwareType = default; + _ = NativeMethods.GetFirmwareType(ref firmwareType); + return firmwareType; + } + + /// + /// Returns the last Win32 Error code thrown by a native method if enabled for this method. + /// + /// The error code as int value. + public static int GetLastError() + { + return Marshal.GetLastPInvokeError(); + } + + /// + /// Validate that the handle is not null and close it. + /// + /// Handle to close. + /// Zero if native method fails and nonzero if the native method succeeds. + public static bool CloseHandleIfNotNull(IntPtr handle) + { + if (handle == IntPtr.Zero) + { + // Return true if there is nothing to close. + return true; + } + + return NativeMethods.CloseHandle(handle); + } + + /// + /// Gets the description for an HRESULT error code. + /// + /// The HRESULT number + /// A string containing the description. + public static string MessageFromHResult(int hr) + { + return Marshal.GetExceptionForHR(hr)?.Message ?? string.Empty; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj new file mode 100644 index 0000000000..7114d3c59d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj @@ -0,0 +1,27 @@ + + + + enable + Microsoft.CmdPal.Ext.System + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + Microsoft.CmdPal.Ext.System + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/OpenInShellCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/OpenInShellCommand.cs new file mode 100644 index 0000000000..a70898a2ad --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/OpenInShellCommand.cs @@ -0,0 +1,33 @@ +// 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.System.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System; + +public sealed partial class OpenInShellCommand : InvokableCommand +{ + public OpenInShellCommand(string name, string path, string? arguments = null, string? workingDir = null, OpenInShellHelper.ShellRunAsType runAs = OpenInShellHelper.ShellRunAsType.None, bool runWithHiddenWindow = false) + { + Name = name; + _path = path; + _arguments = arguments; + _workingDir = workingDir; + _runAs = runAs; + _runWithHiddenWindow = runWithHiddenWindow; + } + + public override CommandResult Invoke() + { + OpenInShellHelper.OpenInShell(_path, _arguments); + return CommandResult.Dismiss(); + } + + private string _path; + private string? _workingDir; + private string? _arguments; + private OpenInShellHelper.ShellRunAsType _runAs; + private bool _runWithHiddenWindow; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Pages/SystemCommandPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Pages/SystemCommandPage.cs new file mode 100644 index 0000000000..d10a4e37ae --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Pages/SystemCommandPage.cs @@ -0,0 +1,24 @@ +// 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.System.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Pages; + +public sealed partial class SystemCommandPage : ListPage +{ + private SettingsManager _settingsManager; + + public SystemCommandPage(SettingsManager settingsManager) + { + Title = Resources.Microsoft_plugin_ext_system_page_name; + Icon = new IconInfo("\uE72E"); + _settingsManager = settingsManager; + ShowDetails = true; + } + + public override IListItem[] GetItems() => Commands.GetAllCommands(_settingsManager).ToArray(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..76db4cb232 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs @@ -0,0 +1,810 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.System { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.System.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Show a dialog to confirm system commands. + /// + public static string confirm_system_commands { + get { + return ResourceManager.GetString("confirm_system_commands", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do you want to empty your Recycle Bin?. + /// + public static string EmptyRecycleBin_ConfirmationDialog_Description { + get { + return ResourceManager.GetString("EmptyRecycleBin_ConfirmationDialog_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty. + /// + public static string Microsoft_plugin_command_name_empty { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hibernate. + /// + public static string Microsoft_plugin_command_name_hibernate { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_hibernate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock. + /// + public static string Microsoft_plugin_command_name_lock { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_lock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string Microsoft_plugin_command_name_open { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reboot. + /// + public static string Microsoft_plugin_command_name_reboot { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_reboot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart. + /// + public static string Microsoft_plugin_command_name_restart { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_restart", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shutdown. + /// + public static string Microsoft_plugin_command_name_shutdown { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_shutdown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign Out. + /// + public static string Microsoft_plugin_command_name_signout { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_signout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sleep. + /// + public static string Microsoft_plugin_command_name_sleep { + get { + return ResourceManager.GetString("Microsoft_plugin_command_name_sleep", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection Details. + /// + public static string Microsoft_plugin_ext_connection_details { + get { + return ResourceManager.GetString("Microsoft_plugin_ext_connection_details", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy to clipboard. + /// + public static string Microsoft_plugin_ext_copy { + get { + return ResourceManager.GetString("Microsoft_plugin_ext_copy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide disconnected network info. + /// + public static string Microsoft_plugin_ext_settings_hideDisconnectedNetworkInfo { + get { + return ResourceManager.GetString("Microsoft_plugin_ext_settings_hideDisconnectedNetworkInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows System Command. + /// + public static string Microsoft_plugin_ext_system_page_name { + get { + return ResourceManager.GetString("Microsoft_plugin_ext_system_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adapter name. + /// + public static string Microsoft_plugin_sys_AdapterName { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_AdapterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cable. + /// + public static string Microsoft_plugin_sys_Cable { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Cable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please confirm.. + /// + public static string Microsoft_plugin_sys_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connected. + /// + public static string Microsoft_plugin_sys_Connected { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Connected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection name. + /// + public static string Microsoft_plugin_sys_ConnectionName { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_ConnectionName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DHCP servers. + /// + public static string Microsoft_plugin_sys_Dhcp { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Dhcp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disconnected. + /// + public static string Microsoft_plugin_sys_Disconnected { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Disconnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DNS servers. + /// + public static string Microsoft_plugin_sys_Dns { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Dns", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default Gateway. + /// + public static string Microsoft_plugin_sys_Gateways { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Gateways", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} Gbps. + /// + public static string Microsoft_plugin_sys_Gbps { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Gbps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hibernate. + /// + public static string Microsoft_plugin_sys_hibernate { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_hibernate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to put this computer into hibernation, are you sure?. + /// + public static string Microsoft_plugin_sys_hibernate_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_hibernate_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hibernate computer. + /// + public static string Microsoft_plugin_sys_hibernate_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_hibernate_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv4 address of {0}. + /// + public static string Microsoft_plugin_sys_ip4_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_ip4_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv4 address. + /// + public static string Microsoft_plugin_sys_Ip4Address { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip4Address", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv4 subnet mask. + /// + public static string Microsoft_plugin_sys_Ip4SubnetMask { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip4SubnetMask", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 address of {0}. + /// + public static string Microsoft_plugin_sys_ip6_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_ip6_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 Address. + /// + public static string Microsoft_plugin_sys_Ip6Address { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip6Address", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 link-local address. + /// + public static string Microsoft_plugin_sys_Ip6Link { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip6Link", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 site-local address. + /// + public static string Microsoft_plugin_sys_Ip6Site { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip6Site", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 temporary address. + /// + public static string Microsoft_plugin_sys_Ip6Temp { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip6Temp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 unique local address. + /// + public static string Microsoft_plugin_sys_Ip6Unique { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Ip6Unique", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock. + /// + public static string Microsoft_plugin_sys_lock { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_lock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to lock this computer, are you sure?. + /// + public static string Microsoft_plugin_sys_lock_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_lock_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock computer. + /// + public static string Microsoft_plugin_sys_lock_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_lock_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loopback. + /// + public static string Microsoft_plugin_sys_Loopback { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Loopback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MAC address of {0} ({1}). + /// + public static string Microsoft_plugin_sys_mac_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_mac_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} Mbps. + /// + public static string Microsoft_plugin_sys_Mbps { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Mbps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mobile broadband. + /// + public static string Microsoft_plugin_sys_MobileBroadband { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_MobileBroadband", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Physical address (MAC). + /// + public static string Microsoft_plugin_sys_PhysicalAddress { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_PhysicalAddress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows System Commands. + /// + public static string Microsoft_plugin_sys_plugin_name { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_plugin_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBin { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty Recycle Bin (Shift+Delete). + /// + public static string Microsoft_plugin_sys_RecycleBin_contextMenu { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_contextMenu", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open the Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBin_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recycle Bin emptied successfully.. + /// + public static string Microsoft_plugin_sys_RecycleBin_EmptySuccessMessage { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_EmptySuccessMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The task to empty the Recycle Bin is already running.. + /// + public static string Microsoft_plugin_sys_RecycleBin_EmptyTaskRunning { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_EmptyTaskRunning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to empty the Recycle Bin:. + /// + public static string Microsoft_plugin_sys_RecycleBin_ErrorMsg { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_ErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recycle Bin is empty.. + /// + public static string Microsoft_plugin_sys_RecycleBin_IsEmpty { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_IsEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBin_searchTag { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_searchTag", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show separate result for Empty Recycle Bin command. + /// + public static string Microsoft_plugin_sys_RecycleBin_ShowEmptySeparate { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_ShowEmptySeparate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show a success message after emptying the Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBin_ShowEmptySuccessMessage { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBin_ShowEmptySuccessMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBinEmpty_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBinEmpty_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Empty Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBinEmptyResult { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBinEmptyResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Recycle Bin. + /// + public static string Microsoft_plugin_sys_RecycleBinOpen { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RecycleBinOpen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart. + /// + public static string Microsoft_plugin_sys_restart_computer { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_restart_computer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to restart this computer, are you sure?. + /// + public static string Microsoft_plugin_sys_restart_computer_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_restart_computer_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart computer. + /// + public static string Microsoft_plugin_sys_restart_computer_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_restart_computer_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ip; mac; address. + /// + public static string Microsoft_plugin_sys_Search_NetworkKeywordList { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Search_NetworkKeywordList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shutdown. + /// + public static string Microsoft_plugin_sys_shutdown_computer { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_shutdown_computer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to shut down this computer, are you sure?. + /// + public static string Microsoft_plugin_sys_shutdown_computer_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_shutdown_computer_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shutdown computer. + /// + public static string Microsoft_plugin_sys_shutdown_computer_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_shutdown_computer_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign out. + /// + public static string Microsoft_plugin_sys_sign_out { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sign_out", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to sign out of this computer, are you sure?. + /// + public static string Microsoft_plugin_sys_sign_out_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sign_out_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign out of computer. + /// + public static string Microsoft_plugin_sys_sign_out_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sign_out_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sleep. + /// + public static string Microsoft_plugin_sys_sleep { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sleep", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to put this computer to sleep, are you sure?. + /// + public static string Microsoft_plugin_sys_sleep_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sleep_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Put computer to sleep. + /// + public static string Microsoft_plugin_sys_sleep_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_sleep_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Speed. + /// + public static string Microsoft_plugin_sys_Speed { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Speed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to State. + /// + public static string Microsoft_plugin_sys_State { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_State", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DNS Suffix. + /// + public static string Microsoft_plugin_sys_Suffix { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Suffix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tunnel. + /// + public static string Microsoft_plugin_sys_TunnelConnection { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_TunnelConnection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + public static string Microsoft_plugin_sys_Type { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UEFI Firmware Settings. + /// + public static string Microsoft_plugin_sys_uefi { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_uefi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?. + /// + public static string Microsoft_plugin_sys_uefi_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_uefi_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reboot computer into UEFI Firmware Settings (Requires administrative permissions.). + /// + public static string Microsoft_plugin_sys_uefi_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_uefi_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown. + /// + public static string Microsoft_plugin_sys_Unknown { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Unknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WINS servers. + /// + public static string Microsoft_plugin_sys_Wins { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_Wins", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wireless LAN. + /// + public static string Microsoft_plugin_sys_WirelessLan { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_WirelessLan", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.resx new file mode 100644 index 0000000000..708dabe0d1 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/Properties/Resources.resx @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Show a dialog to confirm system commands + + + Do you want to empty your Recycle Bin? + + + Empty + Empty recycle bin command name + + + Hibernate + + + Lock + + + Sign Out + + + Open + + + Reboot + + + Restart + + + Shutdown + + + Connection Details + + + Copy to clipboard + + + Hide disconnected network info + + + Windows System Command + + + Adapter name + + + Cable + + + Please confirm. + Request confirmation + + + Connected + + + Connection name + + + DHCP servers + + + Disconnected + + + DNS servers + + + Default Gateway + + + {0} Gbps + Abbreviation of 'Gbits per seconds'. Don't translate the placeholder '{0}' as it is replaced in code. + + + Hibernate + This should align to the action in Windows of a hibernating your computer. + + + You are about to put this computer into hibernation, are you sure? + This should align to the action in Windows of a hibernating your computer. + + + Hibernate computer + This should align to the action in Windows of a hibernating your computer. + + + IPv4 address + + + IPv4 subnet mask + + + IPv4 address of {0} + Don't translate the placeholder '{0}' as it is replaced in code. + + + IPv6 Address + + + IPv6 link-local address + + + IPv6 site-local address + + + IPv6 temporary address + + + IPv6 unique local address + + + IPv6 address of {0} + Don't translate the placeholder '{0}' as it is replaced in code. + + + Lock + This should align to the action in Windows of a locking your computer. + + + You are about to lock this computer, are you sure? + This should align to the action in Windows of a locking your computer. + + + Lock computer + This should align to the action in Windows of a locking your computer. + + + Loopback + + + MAC address of {0} ({1}) + Don't translate the placeholders '{0}' and '{1}' as they are replaced in code. + + + {0} Mbps + Abbreviation of 'Mbits per seconds'. Don't translate the placeholder '{0}' as it is replaced in code. + + + Mobile broadband + + + Physical address (MAC) + + + Windows System Commands + Windows operating system commands. + + + Recycle Bin + Means the recycle bin folder in Explorer. + + + Empty Recycle Bin + This should align to the action in Windows of emptying the recycle bin on your computer. + + + Empty Recycle Bin + This should align to the action in Windows of emptying the recycle bin on your computer. + + + Open Recycle Bin + Means the recycle bin folder in Explorer. + + + Empty Recycle Bin (Shift+Delete) + This should align to the action in Windows of emptying the recycle bin on your computer. + + + Open the Recycle Bin + This should align to the action in Windows of emptying the recycle bin on your computer. + + + Recycle Bin emptied successfully. + Means the recycle bin folder in Explorer. + + + The task to empty the Recycle Bin is already running. + Means the recycle bin folder in Explorer. + + + Failed to empty the Recycle Bin: + + + Recycle Bin is empty. + Means the recycle bin folder in Explorer. + + + Empty Recycle Bin + This should align to the action in Windows of emptying the recycle bin on your computer. + + + Show separate result for Empty Recycle Bin command + + + Show a success message after emptying the Recycle Bin + Means the recycle bin folder in Explorer and "emptying" refers to "Empty Recycle Bin" command. + + + Restart + This should align to the action in Windows of a restarting your computer. + + + You are about to restart this computer, are you sure? + This should align to the action in Windows of a restarting your computer. + + + Restart computer + This should align to the action in Windows of a restarting your computer. + + + ip; mac; address + Translate 'ip' as 'ip' and not as 'ip address'. Same for 'mac'.) + + + Shutdown + This should align to the action in Windows of a shutting down your computer. + + + You are about to shut down this computer, are you sure? + This should align to the action in Windows of a shutting down your computer. + + + Shutdown computer + This should align to the action in Windows of a shutting down your computer. + + + Sign out + This should align to the action in windows of a signing out back to the lock screen. + + + You are about to sign out of this computer, are you sure? + This should align to the action in windows of a signing out back to the lock screen. + + + Sign out of computer + This should align to the action in windows of a signing out back to the lock screen. + + + Sleep + This should align to the action in Windows of a making your computer go to sleep. + + + You are about to put this computer to sleep, are you sure? + This should align to the action in Windows of a making your computer go to sleep. + + + Put computer to sleep + This should align to the action in Windows of a making your computer go to sleep. + + + Speed + + + State + + + DNS Suffix + + + Tunnel + + + Type + Means type like category. Here it means network interface type (ethernet, wifi, ...). + + + UEFI Firmware Settings + This should align to the action in Windows Recovery Environment that restart into uefi settings. + + + You are about to reboot this computer into UEFI Firmware Settings menu, are you sure? + This should align to the action in Windows Recovery Environment that restart into uefi settings. + + + Reboot computer into UEFI Firmware Settings (Requires administrative permissions.) + This should align to the action in Windows Recovery Environment that restart into uefi settings. + + + Unknown + + + WINS servers + + + Wireless LAN + + + Sleep + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs new file mode 100644 index 0000000000..20e977c684 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs @@ -0,0 +1,37 @@ +// 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.System.Helpers; +using Microsoft.CmdPal.Ext.System.Pages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System; + +public partial class SystemCommandExtensionProvider : CommandProvider +{ + private readonly ICommandItem[] _commands; + private static readonly SettingsManager _settingsManager = new(); + public static readonly SystemCommandPage Page = new(_settingsManager); + + public SystemCommandExtensionProvider() + { + DisplayName = Resources.Microsoft_plugin_ext_system_page_name; + _commands = [ + new CommandItem(Page) + { + Title = DisplayName, + Icon = Page.Icon, + MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)], + }, + ]; + + Icon = Page.Icon; + } + + public override ICommandItem[] TopLevelCommands() + { + return _commands; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandsCache.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandsCache.cs new file mode 100644 index 0000000000..ac0811f27f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.System/SystemCommandsCache.cs @@ -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.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.System.Helpers; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.Ext.System; + +public sealed partial class SystemCommandsCache +{ + public SystemCommandsCache(SettingsManager manager) + { + var list = new List(); + var listLock = new object(); + + var a = Task.Run(() => + { + var isBootedInUefiMode = Win32Helpers.GetSystemFirmwareType() == FirmwareType.Uefi; + + var separateEmptyRB = manager.ShowSeparateResultForEmptyRecycleBin; + var confirmSystemCommands = manager.ShowDialogToConfirmCommand; + var showSuccessOnEmptyRB = manager.ShowSuccessMessageAfterEmptyingRecycleBin; + + // normal system commands are fast and can be returned immediately + var systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, separateEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB); + lock (listLock) + { + list.AddRange(systemCommands); + } + }); + + var b = Task.Run(() => + { + // Network (ip and mac) results are slow with many network cards and returned delayed. + // On global queries the first word/part has to be 'ip', 'mac' or 'address' for network results + var networkConnectionResults = Commands.GetNetworkConnectionResults(manager); + lock (listLock) + { + list.AddRange(networkConnectionResults); + } + }); + + Task.WaitAll(a, b); + CachedCommands = list.ToArray(); + } + + public IListItem[] CachedCommands { get; } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.png new file mode 100644 index 0000000000..7ab7b21f35 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.svg new file mode 100644 index 0000000000..aca596ff06 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/TimeDate.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.dark.png new file mode 100644 index 0000000000..1236ba2cc7 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.light.png new file mode 100644 index 0000000000..863c941f11 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Assets/Warning.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs new file mode 100644 index 0000000000..9393d82b6f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs @@ -0,0 +1,84 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; + +[assembly: InternalsVisibleTo("Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests")] + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +internal class AvailableResult +{ + /// + /// Gets or sets the time/date value + /// + internal string Value { get; set; } + + /// + /// Gets or sets the text used for the subtitle and as search term + /// + internal string Label { get; set; } + + /// + /// Gets or sets an alternative search tag that will be evaluated if label doesn't match. For example we like to show the era on searches for 'year' too. + /// + internal string AlternativeSearchTag { get; set; } + + /// + /// Gets or sets a value indicating the type of result + /// + internal ResultIconType IconType { get; set; } + + /// + /// Returns the path to the icon + /// + /// Theme + /// Path + public IconInfo GetIconInfo() + { + return IconType switch + { + ResultIconType.Time => ResultHelper.TimeIcon, + ResultIconType.Date => ResultHelper.CalendarIcon, + ResultIconType.DateTime => ResultHelper.TimeDateIcon, + _ => null, + }; + } + + public ListItem ToListItem() + { + return new ListItem(new CopyTextCommand(this.Value)) + { + Title = this.Value, + Subtitle = this.Label, + Icon = this.GetIconInfo(), + }; + } + + public int Score(string query, string label, string tags) + { + // Get match for label (or for tags if label score is <1) + var score = StringMatcher.FuzzySearch(query, label).Score; + if (score < 1) + { + foreach (var t in tags.Split(";")) + { + var tagScore = StringMatcher.FuzzySearch(query, t.Trim()).Score / 2; + if (tagScore > score) + { + score = tagScore / 2; + } + } + } + + return score; + } +} + +public enum ResultIconType +{ + Time, + Date, + DateTime, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs new file mode 100644 index 0000000000..f01e6b8b07 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +internal static class AvailableResultsList +{ + /// + /// Returns a list with all available date time formats + /// + /// Required for UnitTest: Show time in long format + /// Required for UnitTest: Show date in long format + /// Use custom object to calculate results instead of the system date/time + /// Required for UnitTest: Use custom first week of the year instead of the plugin setting. + /// Required for UnitTest: Use custom first day of the week instead the plugin setting. + /// List of results + internal static List GetList(bool isKeywordSearch, SettingsManager settings, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null) + { + var results = new List(); + var calendar = CultureInfo.CurrentCulture.Calendar; + + var timeExtended = timeLongFormat ?? settings.TimeWithSecond; + var dateExtended = dateLongFormat ?? settings.DateWithWeekday; + var isSystemDateTime = timestamp == null; + var dateTimeNow = timestamp ?? DateTime.Now; + var dateTimeNowUtc = dateTimeNow.ToUniversalTime(); + var firstWeekRule = firstWeekOfYear ?? TimeAndDateHelper.GetCalendarWeekRule(settings.FirstWeekOfYear); + var firstDayOfTheWeek = firstDayOfWeek ?? TimeAndDateHelper.GetFirstDayOfWeek(settings.FirstDayOfWeek); + + results.AddRange(new[] + { + // This range is reserved for the following three results: Time, Date, Now + // Don't add any new result in this range! For new results, please use the next range. + new AvailableResult() + { + Value = dateTimeNow.ToString(TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended), CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Time, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, string.Empty, "Microsoft_plugin_timedate_SearchTagTimeNow"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString(TimeAndDateHelper.GetStringFormat(FormatStringType.Date, timeExtended, dateExtended), CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Date, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, string.Empty, "Microsoft_plugin_timedate_SearchTagDateNow"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString(TimeAndDateHelper.GetStringFormat(FormatStringType.DateTime, timeExtended, dateExtended), CultureInfo.CurrentCulture), + Label = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_DateAndTime", "Microsoft_plugin_timedate_Now"), + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + }); + + if (isKeywordSearch || !settings.OnlyDateTimeNowGlobal) + { + // We use long instead of int for unix time stamp because int is too small after 03:14:07 UTC 2038-01-19 + var unixTimestamp = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeSeconds(); + var unixTimestampMilliseconds = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeMilliseconds(); + var weekOfYear = calendar.GetWeekOfYear(dateTimeNow, firstWeekRule, firstDayOfTheWeek); + var era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow)); + var eraShort = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(calendar.GetEra(dateTimeNow)); + + results.AddRange(new[] + { + new AvailableResult() + { + Value = dateTimeNowUtc.ToString(TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended), CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_TimeUtc, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, string.Empty, "Microsoft_plugin_timedate_SearchTagTimeNow"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = dateTimeNowUtc.ToString(TimeAndDateHelper.GetStringFormat(FormatStringType.DateTime, timeExtended, dateExtended), CultureInfo.CurrentCulture), + Label = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_DateAndTimeUtc", "Microsoft_plugin_timedate_NowUtc"), + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = unixTimestamp.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Unix, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = unixTimestampMilliseconds.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Unix_Milliseconds, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNow.Hour.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Hour, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagTime"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = dateTimeNow.Minute.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Minute, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagTime"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = dateTimeNow.Second.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Second, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagTime"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = dateTimeNow.Millisecond.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Millisecond, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagTime"), + IconType = ResultIconType.Time, + }, + new AvailableResult() + { + Value = DateTimeFormatInfo.CurrentInfo.GetDayName(dateTimeNow.DayOfWeek), + Label = Resources.Microsoft_plugin_timedate_Day, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = TimeAndDateHelper.GetNumberOfDayInWeek(dateTimeNow, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_DayOfWeek, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.Day.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_DayOfMonth, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.DayOfYear.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_DayOfYear, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = TimeAndDateHelper.GetWeekOfMonth(dateTimeNow, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_WeekOfMonth, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = weekOfYear.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_WeekOfYear, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = DateTimeFormatInfo.CurrentInfo.GetMonthName(dateTimeNow.Month), + Label = Resources.Microsoft_plugin_timedate_Month, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.Month.ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_MonthOfYear, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("M", CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_DayMonth, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = calendar.GetYear(dateTimeNow).ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_Year, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = era, + Label = Resources.Microsoft_plugin_timedate_Era, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagEra"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = era != eraShort ? eraShort : string.Empty, // Setting value to empty string if 'era == eraShort'. This result will be filtered later. + Label = Resources.Microsoft_plugin_timedate_EraAbbreviation, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagEra"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("Y", CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_MonthYear, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() + { + Value = dateTimeNow.ToFileTime().ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_WindowsFileTime, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNowUtc.ToString("u"), + Label = Resources.Microsoft_plugin_timedate_UniversalTime, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("s"), + Label = Resources.Microsoft_plugin_timedate_Iso8601, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNowUtc.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture), + Label = Resources.Microsoft_plugin_timedate_Iso8601Utc, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture), + Label = Resources.Microsoft_plugin_timedate_Iso8601Zone, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNowUtc.ToString("yyyy-MM-ddTHH:mm:ss'Z'", CultureInfo.InvariantCulture), + Label = Resources.Microsoft_plugin_timedate_Iso8601ZoneUtc, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("R"), + Label = Resources.Microsoft_plugin_timedate_Rfc1123, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + new AvailableResult() + { + Value = dateTimeNow.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture), + Label = Resources.Microsoft_plugin_timedate_filename_compatible, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.DateTime, + }, + }); + } + + // Return only results where value is not empty + // This can happen, for example, when we can't read the 'era' or when 'era == era abbreviation' and we set value explicitly to an empty string. + return results.Where(x => !string.IsNullOrEmpty(x.Value)).ToList(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs new file mode 100644 index 0000000000..7051894223 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +internal static class ResultHelper +{ + /// + /// Get the string based on the requested type + /// + /// Does the user search for system date/time? + /// Id of the string. (Example: `MyString` for `MyString` and `MyStringNow`) + /// Optional string id for now case + /// The string from the resource file, or otherwise. + internal static string SelectStringFromResources(bool isSystemTimeDate, string stringId, string stringIdNow = default) + { + return !isSystemTimeDate + ? Resources.ResourceManager.GetString(stringId, CultureInfo.CurrentUICulture) ?? string.Empty + : !string.IsNullOrEmpty(stringIdNow) + ? Resources.ResourceManager.GetString(stringIdNow, CultureInfo.CurrentUICulture) ?? string.Empty + : Resources.ResourceManager.GetString(stringId + "Now", CultureInfo.CurrentUICulture) ?? string.Empty; + } + + public static IconInfo TimeIcon { get; } = new IconInfo("\uE823"); + + public static IconInfo CalendarIcon { get; } = new IconInfo("\uE787"); + + public static IconInfo TimeDateIcon { get; } = new IconInfo("\uEC92"); + + /// + /// Gets a result with an error message that only numbers can't be parsed + /// + /// Element of type . + internal static ListItem CreateNumberErrorResult() => new ListItem(new NoOpCommand()) + { + Title = Resources.Microsoft_plugin_timedate_ErrorResultTitle, + Subtitle = Resources.Microsoft_plugin_timedate_ErrorResultSubTitle, + Icon = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.light.png", "Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.dark.png"), + }; + + internal static ListItem CreateInvalidInputErrorResult() => new ListItem(new NoOpCommand()) + { + Title = Resources.Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle, + Subtitle = Resources.Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle, + Icon = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.light.png", "Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.dark.png"), + }; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..d0de0017b8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "timeDate"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _firstWeekOfYearChoices = new() + { + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_Setting_UseSystemSetting, "-1"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstWeekRule_FirstDay, "0"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFullWeek, "1"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFourDayWeek, "2"), + }; + + private static readonly List _firstDayOfWeekChoices = GetFirstDayOfWeekChoices(); + + private static List GetFirstDayOfWeekChoices() + { + // List (Sorted for first day is Sunday) + var list = new List + { + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_Setting_UseSystemSetting, "-1"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Sunday, "0"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Monday, "1"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Tuesday, "2"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Wednesday, "3"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Thursday, "4"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Friday, "5"), + new ChoiceSetSetting.Choice(Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek_Saturday, "6"), + }; + + // Order Rules + var orderRuleSaturday = new string[] { "-1", "6", "0", "1", "2", "3", "4", "5" }; + var orderRuleMonday = new string[] { "-1", "1", "2", "3", "4", "5", "6", "0" }; + + switch (DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek) + { + case DayOfWeek.Saturday: + return list.OrderBy(x => Array.IndexOf(orderRuleSaturday, x.Value)).ToList(); + case DayOfWeek.Monday: + return list.OrderBy(x => Array.IndexOf(orderRuleMonday, x.Value)).ToList(); + default: + // DayOfWeek.Sunday + return list; + } + } + + private readonly ChoiceSetSetting _firstWeekOfYear = new( + Namespaced(nameof(FirstWeekOfYear)), + Resources.Microsoft_plugin_timedate_SettingFirstWeekRule, + Resources.Microsoft_plugin_timedate_SettingFirstWeekRule_Description, + _firstWeekOfYearChoices); + + private readonly ChoiceSetSetting _firstDayOfWeek = new( + Namespaced(nameof(FirstDayOfWeek)), + Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek, + Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek, + _firstDayOfWeekChoices); + + private readonly ToggleSetting _onlyDateTimeNowGlobal = new( + Namespaced(nameof(OnlyDateTimeNowGlobal)), + Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal, + Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description, + true); // TODO -- double check default value + + private readonly ToggleSetting _timeWithSeconds = new( + Namespaced(nameof(TimeWithSecond)), + Resources.Microsoft_plugin_timedate_SettingTimeWithSeconds, + Resources.Microsoft_plugin_timedate_SettingTimeWithSeconds_Description, + false); // TODO -- double check default value + + private readonly ToggleSetting _dateWithWeekday = new( + Namespaced(nameof(DateWithWeekday)), + Resources.Microsoft_plugin_timedate_SettingDateWithWeekday, + Resources.Microsoft_plugin_timedate_SettingDateWithWeekday_Description, + false); // TODO -- double check default value + + private readonly ToggleSetting _hideNumberMessageOnGlobalQuery = new( + Namespaced(nameof(HideNumberMessageOnGlobalQuery)), + Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery, + Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery, + true); // TODO -- double check default value + + public int FirstWeekOfYear + { + get + { + if (_firstWeekOfYear.Value == null || string.IsNullOrEmpty(_firstWeekOfYear.Value)) + { + return -1; + } + + var success = int.TryParse(_firstWeekOfYear.Value, out var result); + + if (!success) + { + return -1; + } + + return result; + } + } + + public int FirstDayOfWeek + { + get + { + if (_firstDayOfWeek.Value == null || string.IsNullOrEmpty(_firstDayOfWeek.Value)) + { + return -1; + } + + var success = int.TryParse(_firstDayOfWeek.Value, out var result); + + if (!success) + { + return -1; + } + + return result; + } + } + + public bool OnlyDateTimeNowGlobal => _onlyDateTimeNowGlobal.Value; + + public bool TimeWithSecond => _timeWithSeconds.Value; + + public bool DateWithWeekday => _dateWithWeekday.Value; + + public bool HideNumberMessageOnGlobalQuery => _hideNumberMessageOnGlobalQuery.Value; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_firstWeekOfYear); + Settings.Add(_firstDayOfWeek); + Settings.Add(_onlyDateTimeNowGlobal); + Settings.Add(_timeWithSeconds); + Settings.Add(_dateWithWeekday); + Settings.Add(_hideNumberMessageOnGlobalQuery); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs new file mode 100644 index 0000000000..3450eda627 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +internal static class TimeAndDateHelper +{ + /// + /// Get the format for the time string + /// + /// Type of format + /// Show date with weekday and name of month (long format) + /// Show time with seconds (long format) + /// String that identifies the time/date format () + internal static string GetStringFormat(FormatStringType targetFormat, bool timeLong, bool dateLong) + { + switch (targetFormat) + { + case FormatStringType.Time: + return timeLong ? "T" : "t"; + case FormatStringType.Date: + return dateLong ? "D" : "d"; + case FormatStringType.DateTime: + if (timeLong & dateLong) + { + return "F"; // Friday, October 31, 2008 5:04:32 PM + } + else if (timeLong & !dateLong) + { + return "G"; // 10/31/2008 5:04:32 PM + } + else if (!timeLong & dateLong) + { + return "f"; // Friday, October 31, 2008 5:04 PM + } + else + { + // (!timeLong & !dateLong) + return "g"; // 10/31/2008 5:04 PM + } + + default: + return string.Empty; // Windows default based on current culture settings + } + } + + /// + /// Returns the number week in the month (Used code from 'David Morton' from ) + /// + /// date + /// Number of week in the month + internal static int GetWeekOfMonth(DateTime date, DayOfWeek formatSettingFirstDayOfWeek) + { + var beginningOfMonth = new DateTime(date.Year, date.Month, 1); + var adjustment = 1; // We count from 1 to 7 and not from 0 to 6 + + while (date.Date.AddDays(1).DayOfWeek != formatSettingFirstDayOfWeek) + { + date = date.AddDays(1); + } + + return (int)Math.Truncate((double)date.Subtract(beginningOfMonth).TotalDays / 7f) + adjustment; + } + + /// + /// Returns the number of the day in the week + /// + /// Date + /// Number of the day in the week + internal static int GetNumberOfDayInWeek(DateTime date, DayOfWeek formatSettingFirstDayOfWeek) + { + var daysInWeek = 7; + var adjustment = 1; // We count from 1 to 7 and not from 0 to 6 + + return ((date.DayOfWeek + daysInWeek - formatSettingFirstDayOfWeek) % daysInWeek) + adjustment; + } + + /// + /// Convert input string to a object in local time + /// + /// String with date/time + /// The new object + /// True on success, otherwise false + internal static bool ParseStringAsDateTime(in string input, out DateTime timestamp) + { + if (DateTime.TryParse(input, out timestamp)) + { + // Known date/time format + return true; + } + else if (Regex.IsMatch(input, @"^u[\+-]?\d{1,10}$") && long.TryParse(input.TrimStart('u'), out var secondsU)) + { + // Unix time stamp + // We use long instead of int, because int is too small after 03:14:07 UTC 2038-01-19 + timestamp = DateTimeOffset.FromUnixTimeSeconds(secondsU).LocalDateTime; + return true; + } + else if (Regex.IsMatch(input, @"^ums[\+-]?\d{1,13}$") && long.TryParse(input.TrimStart("ums".ToCharArray()), out var millisecondsUms)) + { + // Unix time stamp in milliseconds + // We use long instead of int because int is too small after 03:14:07 UTC 2038-01-19 + timestamp = DateTimeOffset.FromUnixTimeMilliseconds(millisecondsUms).LocalDateTime; + return true; + } + else if (Regex.IsMatch(input, @"^ft\d+$") && long.TryParse(input.TrimStart("ft".ToCharArray()), out var secondsFt)) + { + // Windows file time + // DateTime.FromFileTime returns as local time. + timestamp = DateTime.FromFileTime(secondsFt); + return true; + } + else + { + timestamp = new DateTime(1, 1, 1, 1, 1, 1); + return false; + } + } + + /// + /// Test if input is special parsing for Unix time, Unix time in milliseconds or File time. + /// + /// String with date/time + /// True if yes, otherwise false + internal static bool IsSpecialInputParsing(string input) + { + return Regex.IsMatch(input, @"^.*(u|ums|ft)\d"); + } + + /// + /// Returns a CalendarWeekRule enum value based on the plugin setting. + /// + internal static CalendarWeekRule GetCalendarWeekRule(int pluginSetting) + { + switch (pluginSetting) + { + case 0: + return CalendarWeekRule.FirstDay; + case 1: + return CalendarWeekRule.FirstFullWeek; + case 2: + return CalendarWeekRule.FirstFourDayWeek; + default: + // Wrong json value and system setting (-1). + return DateTimeFormatInfo.CurrentInfo.CalendarWeekRule; + } + } + + /// + /// Returns a DayOfWeek enum value based on the FirstDayOfWeek plugin setting. + /// + internal static DayOfWeek GetFirstDayOfWeek(int pluginSetting) + { + switch (pluginSetting) + { + case 0: + return DayOfWeek.Sunday; + case 1: + return DayOfWeek.Monday; + case 2: + return DayOfWeek.Tuesday; + case 3: + return DayOfWeek.Wednesday; + case 4: + return DayOfWeek.Thursday; + case 5: + return DayOfWeek.Friday; + case 6: + return DayOfWeek.Saturday; + default: + // Wrong json value and system setting (-1). + return DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek; + } + } +} + +/// +/// Type of time/date format +/// +internal enum FormatStringType +{ + Time, + Date, + DateTime, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs new file mode 100644 index 0000000000..5022236678 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs @@ -0,0 +1,108 @@ +// 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.RegularExpressions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; + +public sealed partial class TimeDateCalculator +{ + /// + /// Var that holds the delimiter between format and date + /// + private const string InputDelimiter = "::"; + + /// + /// A list of conjunctions that we ignore on search + /// + private static readonly string[] _conjunctionList = Resources.Microsoft_plugin_timedate_Search_ConjunctionList.Split("; "); + + /// + /// Searches for results + /// + /// Search query object + /// List of Wox s. + public static List ExecuteSearch(SettingsManager settings, string query) + { + var isEmptySearchInput = string.IsNullOrEmpty(query); + List availableFormats = new List(); + List results = new List(); + + // currently, all of the search in V2 is keyword search. + var isKeywordSearch = true; + + // Switch search type + if (isEmptySearchInput || (!isKeywordSearch && settings.OnlyDateTimeNowGlobal)) + { + // Return all results for system time/date on empty keyword search + // or only time, date and now results for system time on global queries if the corresponding setting is enabled + availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings)); + } + else if (Regex.IsMatch(query, @".+" + Regex.Escape(InputDelimiter) + @".+")) + { + // Search for specified format with specified time/date value + var userInput = query.Split(InputDelimiter); + if (TimeAndDateHelper.ParseStringAsDateTime(userInput[1], out DateTime timestamp)) + { + availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings, null, null, timestamp)); + query = userInput[0]; + } + } + else if (TimeAndDateHelper.ParseStringAsDateTime(query, out DateTime timestamp)) + { + // Return all formats for specified time/date value + availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings, null, null, timestamp)); + query = string.Empty; + } + else + { + // Search for specified format with system time/date (All other cases) + availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings)); + } + + // Check searchTerm after getting results to select type of result list + if (string.IsNullOrEmpty(query)) + { + // Generate list with all results + foreach (var f in availableFormats) + { + results.Add(f.ToListItem()); + } + } + else + { + // Generate filtered list of results + foreach (var f in availableFormats) + { + var score = f.Score(query, f.Label, f.AlternativeSearchTag); + + if (score > 0) + { + results.Add(f.ToListItem()); + } + } + } + + // If search term is only a number that can't be parsed return an error message + if (!isEmptySearchInput && results.Count == 0 && Regex.IsMatch(query, @"\w+\d+.*$") && !query.Any(char.IsWhiteSpace) && (TimeAndDateHelper.IsSpecialInputParsing(query) || !Regex.IsMatch(query, @"\d+[\.:/]\d+"))) + { + // Without plugin key word show only if message is not hidden by setting + if (!settings.HideNumberMessageOnGlobalQuery) + { + results.Add(ResultHelper.CreateNumberErrorResult()); + } + } + + if (results.Count == 0) + { + results.Add(ResultHelper.CreateInvalidInputErrorResult()); + } + + return results; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj new file mode 100644 index 0000000000..733ed8634e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj @@ -0,0 +1,43 @@ + + + + + Microsoft.CmdPal.Ext.TimeDate + false + false + + Microsoft.CmdPal.Ext.TimeDate.pri + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + Microsoft.CmdPal.Ext.TimeDate + + + + + + + + + + True + True + Resources.resx + + + + + + + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs new file mode 100644 index 0000000000..bb5b58adb6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs @@ -0,0 +1,57 @@ +// 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.TimeDate.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.TimeDate.Pages; + +internal sealed partial class TimeDateExtensionPage : DynamicListPage +{ + private SettingsManager _settingsManager; + + public TimeDateExtensionPage(SettingsManager settingsManager) + { + Icon = IconHelpers.FromRelativePath("Assets\\TimeDate.svg"); + Title = Resources.Microsoft_plugin_timedate_main_page_title; + Name = Resources.Microsoft_plugin_timedate_main_page_name; + PlaceholderText = Resources.Microsoft_plugin_timedate_placeholder_text; + Id = "com.microsoft.cmdpal.timedate"; + _settingsManager = settingsManager; + } + + public override IListItem[] GetItems() => DoExecuteSearch(SearchText).ToArray(); + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + DoExecuteSearch(newSearch); + RaiseItemsChanged(0); + } + + private List DoExecuteSearch(string query) + { + try + { + var result = TimeDateCalculator.ExecuteSearch(_settingsManager, query); + return result; + } + catch (Exception) + { + // sometimes, user's input may not correct. + // In most of the time, user may not have completed their input. + // So, we need to clean the result. + // But in that time, empty result may cause exception. + // So, we need to add at least on item to user. + var items = new List + { + ResultHelper.CreateInvalidInputErrorResult(), + }; + + return items; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..0d6be76c7e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs @@ -0,0 +1,783 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.TimeDate { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.TimeDate.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Copy failed. + /// + public static string Microsoft_plugin_timedate_copy_failed { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_copy_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy value (Ctrl+C). + /// + public static string Microsoft_plugin_timedate_CopyToClipboard { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_CopyToClipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date. + /// + public static string Microsoft_plugin_timedate_Date { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Date", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time. + /// + public static string Microsoft_plugin_timedate_DateAndTime { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DateAndTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time UTC. + /// + public static string Microsoft_plugin_timedate_DateAndTimeUtc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DateAndTimeUtc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Day (Week day). + /// + public static string Microsoft_plugin_timedate_Day { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Day", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Month and day. + /// + public static string Microsoft_plugin_timedate_DayMonth { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DayMonth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Day of the month. + /// + public static string Microsoft_plugin_timedate_DayOfMonth { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DayOfMonth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Day of the week (Week day). + /// + public static string Microsoft_plugin_timedate_DayOfWeek { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DayOfWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Day of the year. + /// + public static string Microsoft_plugin_timedate_DayOfYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DayOfYear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Era. + /// + public static string Microsoft_plugin_timedate_Era { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Era", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Era abbreviation. + /// + public static string Microsoft_plugin_timedate_EraAbbreviation { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_EraAbbreviation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time. + /// + public static string Microsoft_plugin_timedate_ErrorResultSubTitle { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorResultSubTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error: Invalid number input. + /// + public static string Microsoft_plugin_timedate_ErrorResultTitle { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorResultTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time in filename-compatible format. + /// + public static string Microsoft_plugin_timedate_filename_compatible { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_filename_compatible", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hour. + /// + public static string Microsoft_plugin_timedate_Hour { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Hour", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time. + /// + public static string Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error: Invalid input. + /// + public static string Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ISO 8601. + /// + public static string Microsoft_plugin_timedate_Iso8601 { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Iso8601", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ISO 8601 UTC. + /// + public static string Microsoft_plugin_timedate_Iso8601Utc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Iso8601Utc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ISO 8601 with time zone. + /// + public static string Microsoft_plugin_timedate_Iso8601Zone { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Iso8601Zone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ISO 8601 UTC with time zone. + /// + public static string Microsoft_plugin_timedate_Iso8601ZoneUtc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Iso8601ZoneUtc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string Microsoft_plugin_timedate_main_page_name { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_main_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time &amp; Date. + /// + public static string Microsoft_plugin_timedate_main_page_title { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_main_page_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Millisecond. + /// + public static string Microsoft_plugin_timedate_Millisecond { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Millisecond", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minute. + /// + public static string Microsoft_plugin_timedate_Minute { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Minute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Month. + /// + public static string Microsoft_plugin_timedate_Month { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Month", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Month of the year. + /// + public static string Microsoft_plugin_timedate_MonthOfYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_MonthOfYear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Month and year. + /// + public static string Microsoft_plugin_timedate_MonthYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_MonthYear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Now. + /// + public static string Microsoft_plugin_timedate_Now { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Now", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Now UTC. + /// + public static string Microsoft_plugin_timedate_NowUtc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_NowUtc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search values or type a custom time stamp.... + /// + public static string Microsoft_plugin_timedate_placeholder_text { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_placeholder_text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provides time and date values in different formats. + /// + public static string Microsoft_plugin_timedate_plugin_description { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calendar week. + /// + public static string Microsoft_plugin_timedate_plugin_description_example_calendarWeek { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_plugin_description_example_calendarWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Day. + /// + public static string Microsoft_plugin_timedate_plugin_description_example_day { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_plugin_description_example_day", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time. + /// + public static string Microsoft_plugin_timedate_plugin_description_example_time { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_plugin_description_example_time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time and Date. + /// + public static string Microsoft_plugin_timedate_plugin_name { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_plugin_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RFC1123. + /// + public static string Microsoft_plugin_timedate_Rfc1123 { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Rfc1123", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to for; and; nor; but; or; so. + /// + public static string Microsoft_plugin_timedate_Search_ConjunctionList { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Search_ConjunctionList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date. + /// + public static string Microsoft_plugin_timedate_SearchTagDate { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current date; Now. + /// + public static string Microsoft_plugin_timedate_SearchTagDateNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Year; Calendar era; Date. + /// + public static string Microsoft_plugin_timedate_SearchTagEra { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagEra", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current year; Current calendar era; Current date; Now. + /// + public static string Microsoft_plugin_timedate_SearchTagEraNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagEraNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time; Time and Date. + /// + public static string Microsoft_plugin_timedate_SearchTagFormat { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current date and time; Current time and date; Now. + /// + public static string Microsoft_plugin_timedate_SearchTagFormatNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time. + /// + public static string Microsoft_plugin_timedate_SearchTagTime { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current Time; Now. + /// + public static string Microsoft_plugin_timedate_SearchTagTimeNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Second. + /// + public static string Microsoft_plugin_timedate_Second { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Second", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use system setting. + /// + public static string Microsoft_plugin_timedate_Setting_UseSystemSetting { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Setting_UseSystemSetting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show date with weekday and name of month. + /// + public static string Microsoft_plugin_timedate_SettingDateWithWeekday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDateWithWeekday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting applies to the 'Date' and 'Now' result.. + /// + public static string Microsoft_plugin_timedate_SettingDateWithWeekday_Description { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingDateWithWeekday_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First day of the week. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Friday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Friday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Friday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Monday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Monday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Monday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Saturday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Saturday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Saturday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sunday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Sunday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Sunday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Thursday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Thursday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Thursday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tuesday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Tuesday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Tuesday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wednesday. + /// + public static string Microsoft_plugin_timedate_SettingFirstDayOfWeek_Wednesday { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstDayOfWeek_Wednesday", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First week of the year. + /// + public static string Microsoft_plugin_timedate_SettingFirstWeekRule { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstWeekRule", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configure the calendar rule for the first week of the year.. + /// + public static string Microsoft_plugin_timedate_SettingFirstWeekRule_Description { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstWeekRule_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First day of year. + /// + public static string Microsoft_plugin_timedate_SettingFirstWeekRule_FirstDay { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstWeekRule_FirstDay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First four day week. + /// + public static string Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFourDayWeek { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFourDayWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First full week. + /// + public static string Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFullWeek { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingFirstWeekRule_FirstFullWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide 'Invalid number input' error message on global queries. + /// + public static string Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show only 'Time', 'Date' and 'Now' result for system time on global queries. + /// + public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regardless of this setting, for global queries the first word of the query has to be a complete match.. + /// + public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show time with seconds. + /// + public static string Microsoft_plugin_timedate_SettingTimeWithSeconds { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingTimeWithSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting applies to the 'Time' and 'Now' result.. + /// + public static string Microsoft_plugin_timedate_SettingTimeWithSeconds_Description { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SettingTimeWithSeconds_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select or press Ctrl+C to copy. + /// + public static string Microsoft_plugin_timedate_SubTitleNote { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SubTitleNote", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time. + /// + public static string Microsoft_plugin_timedate_Time { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time UTC. + /// + public static string Microsoft_plugin_timedate_TimeUtc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_TimeUtc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alternative search tags:. + /// + public static string Microsoft_plugin_timedate_ToolTipAlternativeSearchTag { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_ToolTipAlternativeSearchTag", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Universal time format: YYYY-MM-DD hh:mm:ss. + /// + public static string Microsoft_plugin_timedate_UniversalTime { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_UniversalTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unix epoch time. + /// + public static string Microsoft_plugin_timedate_Unix { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Unix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unix epoch time in milliseconds. + /// + public static string Microsoft_plugin_timedate_Unix_Milliseconds { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Unix_Milliseconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Week of the month. + /// + public static string Microsoft_plugin_timedate_WeekOfMonth { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_WeekOfMonth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Week of the year (Calendar week, Week number). + /// + public static string Microsoft_plugin_timedate_WeekOfYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_WeekOfYear", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows file time (Int64 number). + /// + public static string Microsoft_plugin_timedate_WindowsFileTime { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_WindowsFileTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Year. + /// + public static string Microsoft_plugin_timedate_Year { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Year", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx new file mode 100644 index 0000000000..efcddf4ca8 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Copy value (Ctrl+C) + 'Ctrl+C' is a shortcut + + + Copy failed + + + Date + + + Date and time + + + Date and time UTC + 'UTC' means here 'Universal Time Convention' + + + Day (Week day) + + + Month and day + + + Day of the month + + + Day of the week (Week day) + + + Day of the year + + + Era + + + Era abbreviation + + + Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time + + + Error: Invalid number input + + + Hour + + + ISO 8601 + + + ISO 8601 UTC + 'UTC' means here 'Universal Time Convention' + + + ISO 8601 with time zone + + + ISO 8601 UTC with time zone + 'UTC' means here 'Universal Time Convention' + + + Date and time in filename-compatible format + The format allows for embedding in filenames + + + Millisecond + + + Minute + + + Month + + + Month of the year + + + Month and year + + + Now + + + Now UTC + 'UTC' means here 'Universal Time Convention' + + + Provides time and date values in different formats + Do not translate the placeholders like '{0}' because it will be replaced in code. + + + Calendar week + + + Day + + + Time + + + Time and Date + + + RFC1123 + + + Date + Don't change order + + + Current date; Now + Don't change order + + + Year; Calendar era; Date + Don't change order + + + Current year; Current calendar era; Current date; Now + Don't change order + + + Date and time; Time and Date + Don't change order + + + Current date and time; Current time and date; Now + Don't change order + + + Time + Don't change order + + + Current Time; Now + Don't change order + + + for; and; nor; but; or; so + List of conjunctions. We don't add 'yet' because this can be a synonym of 'now' which might be problematic on localized searches. + + + Second + + + Show date with weekday and name of month + + + This setting applies to the 'Date' and 'Now' result. + + + Hide 'Invalid number input' error message on global queries + + + Show only 'Time', 'Date' and 'Now' result for system time on global queries + + + Regardless of this setting, for global queries the first word of the query has to be a complete match. + + + Show time with seconds + + + This setting applies to the 'Time' and 'Now' result. + + + Select or press Ctrl+C to copy + 'Ctrl+C' is a shortcut + + + Time + + + Time UTC + 'UTC' means here 'Universal Time Convention' + + + Alternative search tags: + + + Universal time format: YYYY-MM-DD hh:mm:ss + + + Unix epoch time + + + Week of the month + + + Week of the year (Calendar week, Week number) + + + Windows file time (Int64 number) + + + Year + + + Unix epoch time in milliseconds + + + First day of the week + + + Friday + + + Monday + + + Saturday + + + Sunday + + + Thursday + + + Tuesday + + + Wednesday + + + First week of the year + + + Configure the calendar rule for the first week of the year. + + + First day of year + + + First four day week + + + First full week + + + Use system setting + + + Open + + + Search values or type a custom time stamp... + + + Error: Invalid input + + + Time &amp; Date + + + Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs new file mode 100644 index 0000000000..a76cdb9ee0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Text; +using Microsoft.CmdPal.Ext.TimeDate.Helpers; +using Microsoft.CmdPal.Ext.TimeDate.Pages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.TimeDate; + +public partial class TimeDateCommandsProvider : CommandProvider +{ + private readonly CommandItem _command; + private static readonly SettingsManager _settingsManager = new(); + private static readonly CompositeFormat MicrosoftPluginTimedatePluginDescription = System.Text.CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_plugin_description); + private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager); + + public TimeDateCommandsProvider() + { + DisplayName = Resources.Microsoft_plugin_timedate_plugin_name; + + _command = new CommandItem(_timeDateExtensionPage) + { + Icon = _timeDateExtensionPage.Icon, + Title = Resources.Microsoft_plugin_timedate_plugin_name, + Subtitle = GetTranslatedPluginDescription(), + MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)], + }; + + Icon = _timeDateExtensionPage.Icon; + } + + private string GetTranslatedPluginDescription() + { + // The extra strings for the examples are required for correct translations. + var timeExample = Resources.Microsoft_plugin_timedate_plugin_description_example_time + "::" + DateTime.Now.ToString("T", CultureInfo.CurrentCulture); + var dayExample = Resources.Microsoft_plugin_timedate_plugin_description_example_day + "::" + DateTime.Now.ToString("d", CultureInfo.CurrentCulture); + var calendarWeekExample = Resources.Microsoft_plugin_timedate_plugin_description_example_calendarWeek + "::" + DateTime.Now.ToString("d", CultureInfo.CurrentCulture); + return string.Format(CultureInfo.CurrentCulture, MicrosoftPluginTimedatePluginDescription, Resources.Microsoft_plugin_timedate_plugin_description_example_day, dayExample, timeExample, calendarWeekExample); + } + + public override ICommandItem[] TopLevelCommands() => [_command]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png new file mode 100644 index 0000000000..ce22b2dd9c Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg new file mode 100644 index 0000000000..5028e6371f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs new file mode 100644 index 0000000000..84a1c249ba --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.Json; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; + +public class HistoryItem(string searchString, DateTime timestamp) +{ + public string SearchString { get; private set; } = searchString; + + public DateTime Timestamp { get; private set; } = timestamp; + + public string ToJson() => JsonSerializer.Serialize(this); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..b83ba47a73 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.CmdPal.Ext.WebSearch.Commands; +using Microsoft.CmdPal.Ext.WebSearch.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private readonly string _historyPath; + + private static readonly string _namespace = "websearch"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _choices = + [ + new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none), + new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1), + new ChoiceSetSetting.Choice(Resources.history_5, Resources.history_5), + new ChoiceSetSetting.Choice(Resources.history_10, Resources.history_10), + new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20), + ]; + + private readonly ToggleSetting _globalIfURI = new( + Namespaced(nameof(GlobalIfURI)), + Resources.plugin_global_if_uri, + Resources.plugin_global_if_uri, + false); + + private readonly ChoiceSetSetting _showHistory = new( + Namespaced(nameof(ShowHistory)), + Resources.plugin_show_history, + Resources.plugin_show_history, + _choices); + + public bool GlobalIfURI => _globalIfURI.Value; + + public string ShowHistory => _showHistory.Value ?? string.Empty; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + internal static string HistoryStateJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "websearch_history.json"); + } + + public void SaveHistory(HistoryItem historyItem) + { + if (historyItem == null) + { + return; + } + + try + { + List historyItems; + + // Check if the file exists and load existing history + if (File.Exists(_historyPath)) + { + var existingContent = File.ReadAllText(_historyPath); + historyItems = JsonSerializer.Deserialize>(existingContent) ?? []; + } + else + { + historyItems = []; + } + + // Add the new history item + historyItems.Add(historyItem); + + // Determine the maximum number of items to keep based on ShowHistory + if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0) + { + // Keep only the most recent `maxHistoryItems` items + while (historyItems.Count > maxHistoryItems) + { + historyItems.RemoveAt(0); // Remove the oldest item + } + } + + // Serialize the updated list back to JSON and save it + var historyJson = JsonSerializer.Serialize(historyItems); + File.WriteAllText(_historyPath, historyJson); + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + } + } + + public List LoadHistory() + { + try + { + if (!File.Exists(_historyPath)) + { + return []; + } + + // Read and deserialize JSON into a list of HistoryItem objects + var fileContent = File.ReadAllText(_historyPath); + var historyItems = JsonSerializer.Deserialize>(fileContent) ?? []; + + // Convert each HistoryItem to a ListItem + var listItems = new List(); + foreach (var historyItem in historyItems) + { + listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this)) + { + Title = historyItem.SearchString, + Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting + }); + } + + listItems.Reverse(); + return listItems; + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + return []; + } + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + _historyPath = HistoryStateJsonPath(); + + Settings.Add(_globalIfURI); + Settings.Add(_showHistory); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } + + private void ClearHistory() + { + try + { + if (File.Exists(_historyPath)) + { + // Delete the history file + File.Delete(_historyPath); + + // Log that the history was successfully cleared + ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." }); + } + else + { + // Log that there was no history file to delete + ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." }); + } + } + catch (Exception ex) + { + // Log any exception that occurs + ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" }); + } + } + + public override void SaveSettings() + { + base.SaveSettings(); + try + { + if (ShowHistory == Resources.history_none) + { + ClearHistory(); + } + else if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0) + { + // Trim the history file if there are more items than the new limit + if (File.Exists(_historyPath)) + { + var existingContent = File.ReadAllText(_historyPath); + var historyItems = JsonSerializer.Deserialize>(existingContent) ?? []; + + // Check if trimming is needed + if (historyItems.Count > maxHistoryItems) + { + // Trim the list to keep only the most recent `maxHistoryItems` items + historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList(); + + // Save the trimmed history back to the file + var trimmedHistoryJson = JsonSerializer.Serialize(historyItems); + File.WriteAllText(_historyPath, trimmedHistoryJson); + } + } + } + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj new file mode 100644 index 0000000000..3ddedfcd71 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj @@ -0,0 +1,51 @@ + + + + Microsoft.CmdPal.Ext.WebSearch + enable + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + Resources.resx + True + True + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs new file mode 100644 index 0000000000..406f008a84 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.CmdPal.Ext.WebSearch.Commands; +using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; + +namespace Microsoft.CmdPal.Ext.WebSearch.Pages; + +internal sealed partial class WebSearchListPage : DynamicListPage +{ + private readonly string _iconPath = string.Empty; + private readonly List? _historyItems; + private readonly SettingsManager _settingsManager; + private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); + private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); + private List allItems; + + public WebSearchListPage(SettingsManager settingsManager) + { + Name = Resources.command_item_title; + Title = Resources.command_item_title; + PlaceholderText = Resources.plugin_description; + Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); + allItems = [new(new NoOpCommand()) + { + Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"), + Title = Properties.Resources.plugin_description, + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + } + ]; + Id = "com.microsoft.cmdpal.websearch"; + _settingsManager = settingsManager; + _historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null; + if (_historyItems != null) + { + allItems.AddRange(_historyItems); + } + } + + public List Query(string query) + { + ArgumentNullException.ThrowIfNull(query); + IEnumerable? filteredHistoryItems = null; + + if (_historyItems != null) + { + filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType() : null; + } + + var results = new List(); + + // empty query + if (string.IsNullOrEmpty(query)) + { + results.Add(new ListItem(new SearchWebCommand(string.Empty, _settingsManager)) + { + Title = Properties.Resources.plugin_description, + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + Icon = new IconInfo(_iconPath), + }); + } + else + { + var searchTerm = query; + var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager)) + { + Title = searchTerm, + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + Icon = new IconInfo(_iconPath), + }; + results.Add(result); + } + + if (filteredHistoryItems != null) + { + results.AddRange(filteredHistoryItems); + } + + return results; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + allItems = [.. Query(newSearch)]; + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() => [.. allItems]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..e138b74ecc --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs @@ -0,0 +1,225 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.WebSearch.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.WebSearch.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Search the Web. + /// + public static string command_item_title { + get { + return ResourceManager.GetString("command_item_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Web Search. + /// + public static string extension_name { + get { + return ResourceManager.GetString("extension_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 1. + /// + public static string history_1 { + get { + return ResourceManager.GetString("history_1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 10. + /// + public static string history_10 { + get { + return ResourceManager.GetString("history_10", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 20. + /// + public static string history_20 { + get { + return ResourceManager.GetString("history_20", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 5. + /// + public static string history_5 { + get { + return ResourceManager.GetString("history_5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to None. + /// + public static string history_none { + get { + return ResourceManager.GetString("history_none", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open in default browser. + /// + public static string open_in_default_browser { + get { + return ResourceManager.GetString("open_in_default_browser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to the default browser. + /// + public static string plugin_browser { + get { + return ResourceManager.GetString("plugin_browser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searches the web with your default search engine. + /// + public static string plugin_description { + get { + return ResourceManager.GetString("plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include in global results on queries that are URIs. + /// + public static string plugin_global_if_uri { + get { + return ResourceManager.GetString("plugin_global_if_uri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In the default browser. + /// + public static string plugin_in_browser { + get { + return ResourceManager.GetString("plugin_in_browser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In {0}. + /// + public static string plugin_in_browser_name { + get { + return ResourceManager.GetString("plugin_in_browser_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Web Search. + /// + public static string plugin_name { + get { + return ResourceManager.GetString("plugin_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search the web in {0}. + /// + public static string plugin_open { + get { + return ResourceManager.GetString("plugin_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to open {0}.. + /// + public static string plugin_search_failed { + get { + return ResourceManager.GetString("plugin_search_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Determines the number of history items to show from previous searches. + /// + public static string plugin_show_history { + get { + return ResourceManager.GetString("plugin_show_history", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string settings_page_name { + get { + return ResourceManager.GetString("settings_page_name", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx new file mode 100644 index 0000000000..9caaca6c2f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Search the Web + + + Web Search + + + 1 + + + 10 + + + 20 + + + 5 + + + None + + + Open in default browser + + + the default browser + + + Searches the web with your default search engine + + + Don't include in global results on queries that are URIs + + + In the default browser + + + In {0} + Like "Search the web in {the browser name}" + + + Web Search + + + Search the web in {0} + + + Failed to open {0}. + + + Determines the number of history items to show from previous searches + + + Settings + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs new file mode 100644 index 0000000000..7772d9b8b3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.WebSearch.Commands; +using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WebSearch; + +public partial class WebSearchCommandsProvider : CommandProvider +{ + private readonly SettingsManager _settingsManager = new(); + private readonly FallbackExecuteSearchItem _fallbackItem; + + public WebSearchCommandsProvider() + { + Id = "WebSearch"; + DisplayName = Resources.extension_name; + Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); + Settings = _settingsManager.Settings; + + _fallbackItem = new FallbackExecuteSearchItem(_settingsManager); + } + + public override ICommandItem[] TopLevelCommands() + { + return [new WebSearchTopLevelCommandItem(_settingsManager) + { + MoreCommands = [ + new CommandContextItem(Settings!.SettingsPage), + ], + } + ]; + } + + public override IFallbackCommandItem[]? FallbackCommands() => [_fallbackItem]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs new file mode 100644 index 0000000000..d1d2e5ccad --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.CmdPal.Ext.WebSearch.Commands; +using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Pages; +using Microsoft.CmdPal.Ext.WebSearch.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WebSearch; + +public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler +{ + private readonly SettingsManager _settingsManager; + + public WebSearchTopLevelCommandItem(SettingsManager settingsManager) + : base(new WebSearchListPage(settingsManager)) + { + Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); + SetDefaultTitle(); + _settingsManager = settingsManager; + } + + private void SetDefaultTitle() => Title = Resources.command_item_title; + + public void UpdateQuery(string query) + { + if (string.IsNullOrEmpty(query)) + { + SetDefaultTitle(); + Command = new WebSearchListPage(_settingsManager); + } + else + { + Title = query; + Command = new SearchWebCommand(query, _settingsManager); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.png new file mode 100644 index 0000000000..bae5a34bdf Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.svg new file mode 100644 index 0000000000..2bfe0bb295 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.dark.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.png new file mode 100644 index 0000000000..34525e74c3 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.svg new file mode 100644 index 0000000000..4a0b8c0795 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/Store.light.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.png new file mode 100644 index 0000000000..694fcf38a1 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.svg new file mode 100644 index 0000000000..9aaaa5db7e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WinGet/Assets/WinGet.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs new file mode 100644 index 0000000000..46475fafd0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs @@ -0,0 +1,38 @@ +// 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.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; + +internal sealed partial class CloseWindowCommand : InvokableCommand +{ + private readonly Window _window; + + public CloseWindowCommand(Window window) + { + Icon = new IconInfo("\xE8BB"); + Name = $"{Resources.windowwalker_Close}"; + _window = window; + } + + public override ICommandResult Invoke() + { + if (!_window.IsWindow) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Cannot close the window '{_window.Title}' ({_window.Hwnd}), because it doesn't exist." }); + } + + _window.CloseThisWindow(); + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/ExplorerInfoResultCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/ExplorerInfoResultCommand.cs new file mode 100644 index 0000000000..86038960cf --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/ExplorerInfoResultCommand.cs @@ -0,0 +1,67 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; + +internal sealed partial class ExplorerInfoResultCommand : InvokableCommand +{ + public ExplorerInfoResultCommand() + { + } + + public static bool OpenInShell(string path, string? arguments = null, string? workingDir = null, ShellRunAsType runAs = ShellRunAsType.None, bool runWithHiddenWindow = false) + { + using var process = new Process(); + process.StartInfo.FileName = path; + process.StartInfo.WorkingDirectory = string.IsNullOrWhiteSpace(workingDir) ? string.Empty : workingDir; + process.StartInfo.Arguments = string.IsNullOrWhiteSpace(arguments) ? string.Empty : arguments; + process.StartInfo.WindowStyle = runWithHiddenWindow ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal; + process.StartInfo.UseShellExecute = true; + + if (runAs == ShellRunAsType.Administrator) + { + process.StartInfo.Verb = "RunAs"; + } + else if (runAs == ShellRunAsType.OtherUser) + { + process.StartInfo.Verb = "RunAsUser"; + } + + try + { + process.Start(); + return true; + } + catch (Win32Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Unable to open {path}: {ex.Message}" }); + return false; + } + } + + public override ICommandResult Invoke() + { + OpenInShell("rundll32.exe", "shell32.dll,Options_RunDLL 7"); // "shell32.dll,Options_RunDLL 7" opens the view tab in folder options of explorer. + return CommandResult.Dismiss(); + } + + public enum ShellRunAsType + { + None, + Administrator, + OtherUser, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/KillProcessCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/KillProcessCommand.cs new file mode 100644 index 0000000000..5559e1428d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/KillProcessCommand.cs @@ -0,0 +1,80 @@ +// 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.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; + +internal sealed partial class KillProcessCommand : InvokableCommand +{ + private readonly Window _window; + + public KillProcessCommand(Window window) + { + Icon = new IconInfo("\xE74D"); // Delete symbol + Name = $"{Resources.windowwalker_Kill}"; + _window = window; + } + + /// + /// Method to initiate killing the process of a window + /// + /// Window data + /// True if the PT Run window should close, otherwise false. + private static bool KillProcess(Window window) + { + // Validate process + if (!window.IsWindow || !window.Process.DoesExist || string.IsNullOrEmpty(window.Process.Name) || !window.Process.Name.Equals(WindowProcess.GetProcessNameFromProcessID(window.Process.ProcessID), StringComparison.Ordinal)) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Cannot kill process '{window.Process.Name}' ({window.Process.ProcessID}) of the window '{window.Title}' ({window.Hwnd}), because it doesn't exist." }); + + // TODO GH #86 -- need to figure out how to show status message once implemented on host + return false; + } + + // Request user confirmation + if (SettingsManager.Instance.ConfirmKillProcess) + { + // TODO GH #138, #153 -- need to figure out how to confirm kill process? should this just be the same status thing... maybe not? Need message box? Could be nested context menu. + /* + string messageBody = $"{Resources.wox_plugin_windowwalker_KillMessage}\n" + + $"{window.Process.Name} ({window.Process.ProcessID})\n\n" + + $"{(window.Process.IsUwpApp ? Resources.wox_plugin_windowwalker_KillMessageUwp : Resources.wox_plugin_windowwalker_KillMessageQuestion)}"; + MessageBoxResult messageBoxResult = MessageBox.Show( + messageBody, + Resources.wox_plugin_windowwalker_plugin_name + " - " + Resources.wox_plugin_windowwalker_KillMessageTitle, + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + + if (messageBoxResult == MessageBoxResult.No) + { + return false; + } + */ + } + + // Kill process + window.Process.KillThisProcess(SettingsManager.Instance.KillProcessTree); + return !SettingsManager.Instance.OpenAfterKillAndClose; + } + + public override ICommandResult Invoke() + { + if (KillProcess(_window)) + { + return CommandResult.Dismiss(); + } + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs new file mode 100644 index 0000000000..0bfc1e0396 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs @@ -0,0 +1,50 @@ +// 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; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; + +internal sealed partial class SwitchToWindowCommand : InvokableCommand +{ + private readonly Window? _window; + + public SwitchToWindowCommand(Window? window) + { + Name = Resources.switch_to_command_title; + _window = window; + if (_window != null) + { + var p = Process.GetProcessById((int)_window.Process.ProcessID); + if (p != null) + { + try + { + var processFileName = p.MainModule?.FileName; + Icon = new IconInfo(processFileName); + } + catch + { + } + } + } + } + + public override ICommandResult Invoke() + { + if (_window is null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Cannot switch to the window, because it doesn't exist." }); + return CommandResult.Dismiss(); + } + + _window.SwitchToWindow(); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs new file mode 100644 index 0000000000..cbadadc699 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs @@ -0,0 +1,44 @@ +// 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.WindowWalker.Commands; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.System; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +internal sealed class ContextMenuHelper +{ + internal static List GetContextMenuResults(in WindowWalkerListItem listItem) + { + if (listItem?.Window is not Window windowData) + { + return []; + } + + var contextMenu = new List() + { + new(new CloseWindowCommand(windowData)) + { + RequestedShortcut = KeyChordHelpers.FromModifiers(true, false, false, false, (int)VirtualKey.F4, 0), + }, + }; + + // Hide menu if Explorer.exe is the shell process or the process name is ApplicationFrameHost.exe + // In the first case we would crash the windows ui and in the second case we would kill the generic process for uwp apps. + if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpApp && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) + && !(windowData.Process.IsFullAccessDenied && SettingsManager.Instance.HideKillProcessOnElevatedProcesses)) + { + contextMenu.Add(new CommandContextItem(new KillProcessCommand(windowData)) + { + RequestedShortcut = KeyChordHelpers.FromModifiers(true, false, false, false, (int)VirtualKey.Delete, 0), + }); + } + + return contextMenu; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs new file mode 100644 index 0000000000..760cf9b4ec --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs @@ -0,0 +1,134 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Class housing fuzzy matching methods +/// +internal static class FuzzyMatching +{ + /// + /// Finds the best match (the one with the most + /// number of letters adjacent to each other) and + /// returns the index location of each of the letters + /// of the matches + /// + /// The text to search inside of + /// the text to search for + /// returns the index location of each of the letters of the matches + internal static List FindBestFuzzyMatch(string text, string searchText) + { + ArgumentNullException.ThrowIfNull(searchText); + + ArgumentNullException.ThrowIfNull(text); + + // Using CurrentCulture since this is user facing + searchText = searchText.ToLower(CultureInfo.CurrentCulture); + text = text.ToLower(CultureInfo.CurrentCulture); + + // Create a grid to march matches like + // eg. + // a b c a d e c f g + // a x x + // c x x + var matches = new bool[text.Length, searchText.Length]; + for (var firstIndex = 0; firstIndex < text.Length; firstIndex++) + { + for (var secondIndex = 0; secondIndex < searchText.Length; secondIndex++) + { + matches[firstIndex, secondIndex] = + searchText[secondIndex] == text[firstIndex] ? + true : + false; + } + } + + // use this table to get all the possible matches + List> allMatches = GetAllMatchIndexes(matches); + + // return the score that is the max + var maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0; + List bestMatch = allMatches.Count > 0 ? allMatches[0] : new List(); + + foreach (var match in allMatches) + { + var score = CalculateScoreForMatches(match); + if (score > maxScore) + { + bestMatch = match; + maxScore = score; + } + } + + return bestMatch; + } + + /// + /// Gets all the possible matches to the search string with in the text + /// + /// a table showing the matches as generated by + /// a two dimensional array with the first dimension the text and the second + /// one the search string and each cell marked as an intersection between the two + /// a list of the possible combinations that match the search text + internal static List> GetAllMatchIndexes(bool[,] matches) + { + ArgumentNullException.ThrowIfNull(matches); + + List> results = new List>(); + + for (var secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++) + { + for (var firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++) + { + if (secondIndex == 0 && matches[firstIndex, secondIndex]) + { + results.Add(new List { firstIndex }); + } + else if (matches[firstIndex, secondIndex]) + { + var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList(); + + foreach (var pathSofar in tempList) + { + pathSofar.Add(firstIndex); + } + + results.AddRange(tempList); + } + } + + results = results.Where(x => x.Count == secondIndex + 1).ToList(); + } + + return results.Where(x => x.Count == matches.GetLength(1)).ToList(); + } + + /// + /// Calculates the score for a string + /// + /// the index of the matches + /// an integer representing the score + internal static int CalculateScoreForMatches(List matches) + { + ArgumentNullException.ThrowIfNull(matches); + + var score = 0; + + for (var currentIndex = 1; currentIndex < matches.Count; currentIndex++) + { + var previousIndex = currentIndex - 1; + + score -= matches[currentIndex] - matches[previousIndex]; + } + + return score == 0 ? -10000 : score; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs new file mode 100644 index 0000000000..5b71f7bf88 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs @@ -0,0 +1,58 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Class containing methods to control the live preview +/// +internal sealed class LivePreview +{ + /// + /// Makes sure that a window is excluded from the live preview + /// + /// handle to the window to exclude + public static void SetWindowExclusionFromLivePreview(IntPtr hwnd) + { + var renderPolicy = (uint)DwmNCRenderingPolicies.Enabled; + + _ = NativeMethods.DwmSetWindowAttribute( + hwnd, + 12, + ref renderPolicy, + sizeof(uint)); + } + + /// + /// Activates the live preview + /// + /// the window to show by making all other windows transparent + /// the window which should not be transparent but is not the target window + public static void ActivateLivePreview(IntPtr targetWindow, IntPtr windowToSpare) + { + _ = NativeMethods.DwmpActivateLivePreview( + true, + targetWindow, + windowToSpare, + LivePreviewTrigger.Superbar, + IntPtr.Zero); + } + + /// + /// Deactivates the live preview + /// + public static void DeactivateLivePreview() + { + _ = NativeMethods.DwmpActivateLivePreview( + false, + IntPtr.Zero, + IntPtr.Zero, + LivePreviewTrigger.AltTab, + IntPtr.Zero); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs new file mode 100644 index 0000000000..34e5a2c8c2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs @@ -0,0 +1,129 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Class that represents the state of the desktops windows +/// +internal sealed class OpenWindows +{ + /// + /// Used to enforce single execution of EnumWindows + /// + private static readonly object _enumWindowsLock = new(); + + /// + /// PowerLauncher main executable + /// + private static readonly string? _powerLauncherExe = Path.GetFileName(Environment.ProcessPath); + + /// + /// List of all the open windows + /// + private readonly List windows = new(); + + /// + /// An instance of the class OpenWindows + /// + private static OpenWindows? instance; + + /// + /// Gets the list of all open windows + /// + internal List Windows => new(windows); + + /// + /// Gets an instance property of this class that makes sure that + /// the first instance gets created and that all the requests + /// end up at that one instance + /// + internal static OpenWindows Instance + { + get + { + instance ??= new OpenWindows(); + + return instance; + } + } + + /// + /// Initializes a new instance of the class. + /// Private constructor to make sure there is never + /// more than one instance of this class + /// + private OpenWindows() + { + } + + /// + /// Updates the list of open windows + /// + internal void UpdateOpenWindowsList(CancellationToken cancellationToken) + { + var tokenHandle = GCHandle.Alloc(cancellationToken); + try + { + var tokenHandleParam = GCHandle.ToIntPtr(tokenHandle); + lock (_enumWindowsLock) + { + windows.Clear(); + EnumWindowsProc callbackptr = new EnumWindowsProc(WindowEnumerationCallBack); + _ = NativeMethods.EnumWindows(callbackptr, tokenHandleParam); + } + } + finally + { + if (tokenHandle.IsAllocated) + { + tokenHandle.Free(); + } + } + } + + /// + /// Call back method for window enumeration + /// + /// The handle to the current window being enumerated + /// Value being passed from the caller (we don't use this but might come in handy + /// in the future + /// true to make sure to continue enumeration + internal bool WindowEnumerationCallBack(IntPtr hwnd, IntPtr lParam) + { + var tokenHandle = GCHandle.FromIntPtr(lParam); + var target = (CancellationToken?)tokenHandle.Target ?? CancellationToken.None; + var cancellationToken = target; + if (cancellationToken.IsCancellationRequested) + { + // Stop enumeration + return false; + } + + Window newWindow = new Window(hwnd); + + if (newWindow.IsWindow && newWindow.Visible && newWindow.IsOwner && + (!newWindow.IsToolWindow || newWindow.IsAppWindow) && !newWindow.TaskListDeleted && + (newWindow.Desktop.IsVisible || !SettingsManager.Instance.ResultsFromVisibleDesktopOnly || WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetDesktopCount() < 2) && + newWindow.ClassName != "Windows.UI.Core.CoreWindow" && newWindow.Process.Name != _powerLauncherExe) + { + // To hide (not add) preloaded uwp app windows that are invisible to the user and other cloaked windows, we check the cloak state. (Issue #13637.) + // (If user asking to see cloaked uwp app windows again we can add an optional plugin setting in the future.) + if (!newWindow.IsCloaked || newWindow.GetWindowCloakState() == Window.WindowCloakState.OtherDesktop) + { + windows.Add(newWindow); + } + } + + return true; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs new file mode 100644 index 0000000000..092f1544de --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs @@ -0,0 +1,127 @@ +// 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.WindowWalker.Commands; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Helper class to work with results +/// +internal static class ResultHelper +{ + /// + /// Returns a list of all results for the query. + /// + /// List with all search controller matches + /// List of results + internal static List GetResultList(List searchControllerResults, bool isKeywordSearch) + { + if (searchControllerResults == null || searchControllerResults.Count == 0) + { + return []; + } + + var resultsList = new List(searchControllerResults.Count); + var addExplorerInfo = searchControllerResults.Any(x => + string.Equals(x.Result.Process.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase) && + x.Result.Process.IsShellProcess); + + // Process each SearchResult to convert it into a Result. + // Using parallel processing if the operation is CPU-bound and the list is large. + resultsList = searchControllerResults + .AsParallel() + .Select(x => CreateResultFromSearchResult(x)) + .ToList(); + + if (addExplorerInfo && !SettingsManager.Instance.HideExplorerSettingInfo) + { + resultsList.Insert(0, GetExplorerInfoResult()); + } + + return resultsList; + } + + /// + /// Creates a Result object from a given SearchResult. + /// + /// The SearchResult object to convert. + /// A Result object populated with data from the SearchResult. + private static WindowWalkerListItem CreateResultFromSearchResult(SearchResult searchResult) + { + var item = new WindowWalkerListItem(searchResult.Result) + { + Title = searchResult.Result.Title, + Subtitle = GetSubtitle(searchResult.Result), + Tags = GetTags(searchResult.Result), + }; + item.MoreCommands = ContextMenuHelper.GetContextMenuResults(item).ToArray(); + + return item; + } + + /// + /// Returns the subtitle for a result + /// + /// The window properties of the result + /// String with the subtitle + private static string GetSubtitle(Window window) + { + if (window is null or null) + { + return string.Empty; + } + + var subtitleText = Resources.windowwalker_Running + ": " + window.Process.Name; + + return subtitleText; + } + + private static Tag[] GetTags(Window window) + { + var tags = new List(); + if (!window.Process.IsResponding) + { + tags.Add(new Tag + { + Text = Resources.windowwalker_NotResponding, + Foreground = ColorHelpers.FromRgb(220, 20, 60), + }); + } + + if (SettingsManager.Instance.SubtitleShowPid) + { + tags.Add(new Tag + { + Text = $"{Resources.windowwalker_ProcessId}: {window.Process.ProcessID}", + }); + } + + if (SettingsManager.Instance.SubtitleShowDesktopName && WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetDesktopCount() > 1) + { + tags.Add(new Tag + { + Text = $"{Resources.windowwalker_Desktop}: {window.Desktop.Name}", + }); + } + + return tags.ToArray(); + } + + private static WindowWalkerListItem GetExplorerInfoResult() + { + return new WindowWalkerListItem(null) + { + Title = Resources.windowwalker_ExplorerInfoTitle, + Icon = new IconInfo("\uE946"), // Info + Subtitle = Resources.windowwalker_ExplorerInfoSubTitle, + Command = new ExplorerInfoResultCommand(), + }; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs new file mode 100644 index 0000000000..2e5345bdfd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs @@ -0,0 +1,150 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Responsible for searching and finding matches for the strings provided. +/// Essentially the UI independent model of the application +/// +internal sealed class SearchController +{ + /// + /// the current search text + /// + private string searchText; + + /// + /// Open window search results + /// + private List? searchMatches; + + /// + /// Singleton pattern + /// + private static SearchController? instance; + + /// + /// Gets or sets the current search text + /// + internal string SearchText + { + get => searchText; + + set => + searchText = value.ToLower(CultureInfo.CurrentCulture).Trim(); + } + + /// + /// Gets the open window search results + /// + internal List SearchMatches => new List(searchMatches ?? []).OrderByDescending(x => x.Score).ToList(); + + /// + /// Gets singleton Pattern + /// + internal static SearchController Instance + { + get + { + instance ??= new SearchController(); + + return instance; + } + } + + /// + /// Initializes a new instance of the class. + /// Initializes the search controller object + /// + private SearchController() + { + searchText = string.Empty; + } + + /// + /// Event handler for when the search text has been updated + /// + internal void UpdateSearchText(string searchText) + { + SearchText = searchText; + SyncOpenWindowsWithModel(); + } + + /// + /// Syncs the open windows with the OpenWindows Model + /// + internal void SyncOpenWindowsWithModel() + { + System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model"); + + var snapshotOfOpenWindows = OpenWindows.Instance.Windows; + + searchMatches = string.IsNullOrWhiteSpace(SearchText) ? AllOpenWindows(snapshotOfOpenWindows) : FuzzySearchOpenWindows(snapshotOfOpenWindows); + } + + /// + /// Search method that matches the title of windows with the user search text + /// + /// what windows are open + /// Returns search results + private List FuzzySearchOpenWindows(List openWindows) + { + List result = []; + var searchStrings = new SearchString(searchText, SearchResult.SearchType.Fuzzy); + + foreach (var window in openWindows) + { + var titleMatch = FuzzyMatching.FindBestFuzzyMatch(window.Title, searchStrings.SearchText); + var processMatch = FuzzyMatching.FindBestFuzzyMatch(window.Process.Name ?? string.Empty, searchStrings.SearchText); + + if ((titleMatch.Count != 0 || processMatch.Count != 0) && window.Title.Length != 0) + { + result.Add(new SearchResult(window, titleMatch, processMatch, searchStrings.SearchType)); + } + } + + System.Diagnostics.Debug.Print("Found " + result.Count + " windows that match the search text"); + + return result; + } + + /// + /// Search method that matches all the windows with a title + /// + /// what windows are open + /// Returns search results + private List AllOpenWindows(List openWindows) + { + List result = []; + + foreach (var window in openWindows) + { + if (window.Title.Length != 0) + { + result.Add(new SearchResult(window)); + } + } + + return SettingsManager.Instance.InMruOrder + ? result.ToList() + : result + .OrderBy(w => w.Result.Title) + .ToList(); + } + + /// + /// Event args for a window list update event + /// + internal sealed class SearchResultUpdateEventArgs : EventArgs + { + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs new file mode 100644 index 0000000000..bfe51344ce --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs @@ -0,0 +1,147 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System.Collections.Generic; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Contains search result windows with each window including the reason why the result was included +/// +internal sealed class SearchResult +{ + /// + /// Gets the actual window reference for the search result + /// + internal Window Result + { + get; + private set; + } + + /// + /// Gets the list of indexes of the matching characters for the search in the title window + /// + internal List SearchMatchesInTitle + { + get; + private set; + } + + /// + /// Gets the list of indexes of the matching characters for the search in the + /// name of the process + /// + internal List SearchMatchesInProcessName + { + get; + private set; + } + + /// + /// Gets the type of match (shortcut, fuzzy or nothing) + /// + internal SearchType SearchResultMatchType + { + get; + private set; + } + + /// + /// Gets a score indicating how well this matches what we are looking for + /// + internal int Score + { + get; + private set; + } + + /// + /// Gets the source of where the best score was found + /// + internal TextType BestScoreSource + { + get; + private set; + } + + /// + /// Initializes a new instance of the class. + /// Constructor + /// + internal SearchResult(Window window, List matchesInTitle, List matchesInProcessName, SearchType matchType) + { + Result = window; + SearchMatchesInTitle = matchesInTitle; + SearchMatchesInProcessName = matchesInProcessName; + SearchResultMatchType = matchType; + CalculateScore(); + } + + /// + /// Initializes a new instance of the class. + /// + internal SearchResult(Window window) + { + Result = window; + SearchMatchesInTitle = new List(); + SearchMatchesInProcessName = new List(); + SearchResultMatchType = SearchType.Empty; + CalculateScore(); + } + + /// + /// Calculates the score for how closely this window matches the search string + /// + /// + /// Higher Score is better + /// + private void CalculateScore() + { + if (FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName) > + FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle)) + { + Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName); + BestScoreSource = TextType.ProcessName; + } + else + { + Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle); + BestScoreSource = TextType.WindowTitle; + } + } + + /// + /// The type of text that a string represents + /// + internal enum TextType + { + ProcessName, + WindowTitle, + } + + /// + /// The type of search + /// + internal enum SearchType + { + /// + /// the search string is empty, which means all open windows are + /// going to be returned + /// + Empty, + + /// + /// Regular fuzzy match search + /// + Fuzzy, + + /// + /// The user has entered text that has been matched to a shortcut + /// and the shortcut is now being searched + /// + Shortcut, + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs new file mode 100644 index 0000000000..912dd04ceb --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// A class to represent a search string +/// +/// Class was added inorder to be able to attach various context data to +/// a search string +internal sealed class SearchString +{ + /// + /// Gets where is the search string coming from (is it a shortcut + /// or direct string, etc...) + /// + internal SearchResult.SearchType SearchType + { + get; + private set; + } + + /// + /// Gets the actual text we are searching for + /// + internal string SearchText + { + get; + private set; + } + + /// + /// Initializes a new instance of the class. + /// Constructor + /// + /// text from search + /// type of search + internal SearchString(string searchText, SearchResult.SearchType searchType) + { + SearchText = searchText; + SearchType = searchType; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs new file mode 100644 index 0000000000..276951b30b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs @@ -0,0 +1,357 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Represents a specific open window +/// +internal sealed class Window +{ + /// + /// The handle to the window + /// + private readonly IntPtr hwnd; + + /// + /// A static cache for the process data of all known windows + /// that we don't have to query the data every time + /// + private static readonly Dictionary _handlesToProcessCache = new(); + + /// + /// An instance of that contains the process information for the window + /// + private readonly WindowProcess processInfo; + + /// + /// An instance of that contains the desktop information for the window + /// + private readonly VDesktop desktopInfo; + + /// + /// Gets the title of the window (the string displayed at the top of the window) + /// + internal string Title + { + get + { + var sizeOfTitle = NativeMethods.GetWindowTextLength(hwnd); + if (sizeOfTitle++ > 0) + { + StringBuilder titleBuffer = new StringBuilder(sizeOfTitle); + var numCharactersWritten = NativeMethods.GetWindowText(hwnd, titleBuffer, sizeOfTitle); + if (numCharactersWritten == 0) + { + return string.Empty; + } + + return titleBuffer.ToString(); + } + else + { + return string.Empty; + } + } + } + + /// + /// Gets the handle to the window + /// + internal IntPtr Hwnd => hwnd; + + /// + /// Gets the object of with the process information of the window + /// + internal WindowProcess Process => processInfo; + + /// + /// Gets the object of with the desktop information of the window + /// + internal VDesktop Desktop => desktopInfo; + + /// + /// Gets the name of the class for the window represented + /// + internal string ClassName => GetWindowClassName(Hwnd); + + /// + /// Gets a value indicating whether the window is visible (might return false if it is a hidden IE tab) + /// + internal bool Visible => NativeMethods.IsWindowVisible(Hwnd); + + /// + /// Gets a value indicating whether the window is cloaked (true) or not (false). + /// (A cloaked window is not visible to the user. But the window is still composed by DWM.) + /// + internal bool IsCloaked => GetWindowCloakState() != WindowCloakState.None; + + /// + /// Gets a value indicating whether the specified window handle identifies an existing window. + /// + internal bool IsWindow => NativeMethods.IsWindow(Hwnd); + + /// + /// Gets a value indicating whether the window is a toolwindow + /// + internal bool IsToolWindow => (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) & + (uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW) == + (uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW; + + /// + /// Gets a value indicating whether the window is an appwindow + /// + internal bool IsAppWindow => (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) & + (uint)ExtendedWindowStyles.WS_EX_APPWINDOW) == + (uint)ExtendedWindowStyles.WS_EX_APPWINDOW; + + /// + /// Gets a value indicating whether the window has ITaskList_Deleted property + /// + internal bool TaskListDeleted => NativeMethods.GetProp(Hwnd, "ITaskList_Deleted") != IntPtr.Zero; + + /// + /// Gets a value indicating whether the specified windows is the owner (i.e. doesn't have an owner) + /// + internal bool IsOwner => NativeMethods.GetWindow(Hwnd, GetWindowCmd.GW_OWNER) == IntPtr.Zero; + + /// + /// Gets a value indicating whether the window is minimized + /// + internal bool Minimized => GetWindowSizeState() == WindowSizeState.Minimized; + + /// + /// Initializes a new instance of the class. + /// Initializes a new Window representation + /// + /// the handle to the window we are representing + internal Window(IntPtr hwnd) + { + // TODO: Add verification as to whether the window handle is valid + this.hwnd = hwnd; + processInfo = CreateWindowProcessInstance(hwnd); + desktopInfo = WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetWindowDesktop(hwnd); + } + + /// + /// Switches desktop focus to the window + /// + internal void SwitchToWindow() + { + // The following block is necessary because + // 1) There is a weird flashing behavior when trying + // to use ShowWindow for switching tabs in IE + // 2) SetForegroundWindow fails on minimized windows + // Using Ordinal since this is internal + if (processInfo.Name?.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) == true || !Minimized) + { + NativeMethods.SetForegroundWindow(Hwnd); + } + else + { + if (!NativeMethods.ShowWindow(Hwnd, ShowWindowCommand.Restore)) + { + // ShowWindow doesn't work if the process is running elevated: fallback to SendMessage + _ = NativeMethods.SendMessage(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_RESTORE); + } + } + + NativeMethods.FlashWindow(Hwnd, true); + } + + /// + /// Helper function to close the window + /// + internal void CloseThisWindowHelper() + { + _ = NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_CLOSE, 0, 0x0000, 5000, out _); + } + + /// + /// Closes the window + /// + internal void CloseThisWindow() + { + Thread thread = new(new ThreadStart(CloseThisWindowHelper)); + thread.Start(); + } + + /// + /// Converts the window name to string along with the process name + /// + /// The title of the window + public override string ToString() + { + // Using CurrentCulture since this is user facing + return Title + " (" + processInfo.Name?.ToUpper(CultureInfo.CurrentCulture) + ")"; + } + + /// + /// Returns what the window size is + /// + /// The state (minimized, maximized, etc..) of the window + internal WindowSizeState GetWindowSizeState() + { + NativeMethods.GetWindowPlacement(Hwnd, out WINDOWPLACEMENT placement); + + switch (placement.ShowCmd) + { + case ShowWindowCommand.Normal: + return WindowSizeState.Normal; + case ShowWindowCommand.Minimize: + case ShowWindowCommand.ShowMinimized: + return WindowSizeState.Minimized; + case ShowWindowCommand.Maximize: // No need for ShowMaximized here since its also of value 3 + return WindowSizeState.Maximized; + default: + // throw new Exception("Don't know how to handle window state = " + placement.ShowCmd); + return WindowSizeState.Unknown; + } + } + + /// + /// Enum to simplify the state of the window + /// + internal enum WindowSizeState + { + Normal, + Minimized, + Maximized, + Unknown, + } + + /// + /// Returns the window cloak state from DWM + /// (A cloaked window is not visible to the user. But the window is still composed by DWM.) + /// + /// The state (none, app, ...) of the window + internal WindowCloakState GetWindowCloakState() + { + _ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out var isCloakedState, sizeof(uint)); + + switch (isCloakedState) + { + case (int)DwmWindowCloakStates.None: + return WindowCloakState.None; + case (int)DwmWindowCloakStates.CloakedApp: + return WindowCloakState.App; + case (int)DwmWindowCloakStates.CloakedShell: + return WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell; + case (int)DwmWindowCloakStates.CloakedInherited: + return WindowCloakState.Inherited; + default: + return WindowCloakState.Unknown; + } + } + + /// + /// Enum to simplify the cloak state of the window + /// + internal enum WindowCloakState + { + None, + App, + Shell, + Inherited, + OtherDesktop, + Unknown, + } + + /// + /// Returns the class name of a window. + /// + /// Handle to the window. + /// Class name + private static string GetWindowClassName(IntPtr hwnd) + { + StringBuilder windowClassName = new StringBuilder(300); + var numCharactersWritten = NativeMethods.GetClassName(hwnd, windowClassName, windowClassName.MaxCapacity); + + if (numCharactersWritten == 0) + { + return string.Empty; + } + + return windowClassName.ToString(); + } + + /// + /// Gets an instance of form process cache or creates a new one. A new one will be added to the cache. + /// + /// The handle to the window + /// A new Instance of type + private static WindowProcess CreateWindowProcessInstance(IntPtr hWindow) + { + lock (_handlesToProcessCache) + { + if (_handlesToProcessCache.Count > 7000) + { + Debug.Print("Clearing Process Cache because it's size is " + _handlesToProcessCache.Count); + _handlesToProcessCache.Clear(); + } + + // Add window's process to cache if missing + if (!_handlesToProcessCache.ContainsKey(hWindow)) + { + // Get process ID and name + var processId = WindowProcess.GetProcessIDFromWindowHandle(hWindow); + var threadId = WindowProcess.GetThreadIDFromWindowHandle(hWindow); + var processName = WindowProcess.GetProcessNameFromProcessID(processId); + + if (processName.Length != 0) + { + _handlesToProcessCache.Add(hWindow, new WindowProcess(processId, threadId, processName)); + } + else + { + // For the dwm process we cannot receive the name. This is no problem because the window isn't part of result list. + ExtensionHost.LogMessage(new LogMessage() { Message = $"Invalid process {processId} ({processName}) for window handle {hWindow}." }); + _handlesToProcessCache.Add(hWindow, new WindowProcess(0, 0, string.Empty)); + } + } + + // Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe' + // (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.) + if (string.Equals(_handlesToProcessCache[hWindow].Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) + { + new Task(() => + { + EnumWindowsProc callbackptr = new EnumWindowsProc((IntPtr hwnd, IntPtr lParam) => + { + // Every uwp app main window has at least three child windows. Only the one we are interested in has a class starting with "Windows.UI.Core." and is assigned to the real app process. + // (The other ones have a class name that begins with the string "ApplicationFrame".) + if (GetWindowClassName(hwnd).StartsWith("Windows.UI.Core.", StringComparison.OrdinalIgnoreCase)) + { + var childProcessId = WindowProcess.GetProcessIDFromWindowHandle(hwnd); + var childThreadId = WindowProcess.GetThreadIDFromWindowHandle(hwnd); + var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId); + + // Update process info in cache + _handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName); + return false; + } + else + { + return true; + } + }); + _ = NativeMethods.EnumChildWindows(hWindow, callbackptr, 0); + }).Start(); + } + + return _handlesToProcessCache[hWindow]; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs new file mode 100644 index 0000000000..f0e1639d68 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using System; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.CmdPal.Ext.WindowWalker.Commands; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Represents the process data of an open window. This class is used in the process cache and for the process object of the open window +/// +internal sealed class WindowProcess +{ + /// + /// Maximum size of a file name + /// + private const int MaximumFileNameLength = 1000; + + /// + /// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process + /// + private readonly bool _isUwpApp; + + /// + /// Gets the id of the process + /// + internal uint ProcessID + { + get; private set; + } + + /// + /// Gets a value indicating whether the process is responding or not + /// + internal bool IsResponding + { + get + { + try + { + return Process.GetProcessById((int)ProcessID).Responding; + } + catch (InvalidOperationException) + { + // Thrown when process not exist. + return true; + } + catch (NotSupportedException) + { + // Thrown when process is not running locally. + return true; + } + } + } + + /// + /// Gets the id of the thread + /// + internal uint ThreadID + { + get; private set; + } + + /// + /// Gets the name of the process + /// + internal string? Name + { + get; private set; + } + + /// + /// Gets a value indicating whether the window belongs to an 'Universal Windows Platform (UWP)' process + /// + internal bool IsUwpApp => _isUwpApp; + + /// + /// Gets a value indicating whether this is the shell process or not + /// The shell process (like explorer.exe) hosts parts of the user interface (like taskbar, start menu, ...) + /// + internal bool IsShellProcess + { + get + { + var hShellWindow = NativeMethods.GetShellWindow(); + return GetProcessIDFromWindowHandle(hShellWindow) == ProcessID; + } + } + + /// + /// Gets a value indicating whether the process exists on the machine + /// + internal bool DoesExist + { + get + { + try + { + var p = Process.GetProcessById((int)ProcessID); + p.Dispose(); + return true; + } + catch (InvalidOperationException) + { + // Thrown when process not exist. + return false; + } + catch (ArgumentException) + { + // Thrown when process not exist. + return false; + } + } + } + + /// + /// Gets a value indicating whether full access to the process is denied or not + /// + internal bool IsFullAccessDenied + { + get; private set; + } + + /// + /// Initializes a new instance of the class. + /// + /// New process id. + /// New thread id. + /// New process name. + internal WindowProcess(uint pid, uint tid, string name) + { + UpdateProcessInfo(pid, tid, name); + _isUwpApp = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Updates the process information of the instance. + /// + /// New process id. + /// New thread id. + /// New process name. + internal void UpdateProcessInfo(uint pid, uint tid, string name) + { + // TODO: Add verification as to whether the process id and thread id is valid + ProcessID = pid; + ThreadID = tid; + Name = name; + + // Process can be elevated only if process id is not 0 (Dummy value on error) + IsFullAccessDenied = (pid != 0) ? TestProcessAccessUsingAllAccessFlag(pid) : false; + } + + /// + /// Gets the process ID for the window handle + /// + /// The handle to the window + /// The process ID + internal static uint GetProcessIDFromWindowHandle(IntPtr hwnd) + { + _ = NativeMethods.GetWindowThreadProcessId(hwnd, out var processId); + return processId; + } + + /// + /// Gets the thread ID for the window handle + /// + /// The handle to the window + /// The thread ID + internal static uint GetThreadIDFromWindowHandle(IntPtr hwnd) + { + var threadId = NativeMethods.GetWindowThreadProcessId(hwnd, out _); + return threadId; + } + + /// + /// Gets the process name for the process ID + /// + /// The id of the process/param> + /// A string representing the process name or an empty string if the function fails + internal static string GetProcessNameFromProcessID(uint pid) + { + var processHandle = NativeMethods.OpenProcess(ProcessAccessFlags.QueryLimitedInformation, true, (int)pid); + StringBuilder processName = new StringBuilder(MaximumFileNameLength); + + if (NativeMethods.GetProcessImageFileName(processHandle, processName, MaximumFileNameLength) != 0) + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return processName.ToString().Split('\\').Reverse().ToArray()[0]; + } + else + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return string.Empty; + } + } + + /// + /// Kills the process by it's id. If permissions are required, they will be requested. + /// + /// Kill process and sub processes. + internal void KillThisProcess(bool killProcessTree) + { + if (IsFullAccessDenied) + { + var killTree = killProcessTree ? " /t" : string.Empty; + ExplorerInfoResultCommand.OpenInShell("taskkill.exe", $"/pid {(int)ProcessID} /f{killTree}", null, ExplorerInfoResultCommand.ShellRunAsType.Administrator, true); + } + else + { + Process.GetProcessById((int)ProcessID).Kill(killProcessTree); + } + } + + /// + /// Gets a boolean value indicating whether the access to a process using the AllAccess flag is denied or not. + /// + /// The process ID of the process + /// True if denied and false if not. + private static bool TestProcessAccessUsingAllAccessFlag(uint pid) + { + var processHandle = NativeMethods.OpenProcess(ProcessAccessFlags.AllAccess, true, (int)pid); + + if (Win32Helpers.GetLastError() == 5) + { + // Error 5 = ERROR_ACCESS_DENIED + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return true; + } + else + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return false; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/CVirtualDesktopManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/CVirtualDesktopManager.cs new file mode 100644 index 0000000000..26663dd5f0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/CVirtualDesktopManager.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +/// +/// Virtual Desktop Manager class +/// Code used from +/// +[ComImport] +[Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a")] +internal class CVirtualDesktopManager +{ +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/IVirtualDesktopManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/IVirtualDesktopManager.cs new file mode 100644 index 0000000000..070e24cc26 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/IVirtualDesktopManager.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +/// +/// Interface for accessing Virtual Desktop Manager. +/// Code used from +/// +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")] +[System.Security.SuppressUnmanagedCodeSecurity] +internal interface IVirtualDesktopManager +{ + [PreserveSig] + int IsWindowOnCurrentVirtualDesktop([In] IntPtr hTopLevelWindow, [Out] out int onCurrentDesktop); + + [PreserveSig] + int GetWindowDesktopId([In] IntPtr hTopLevelWindow, [Out] out Guid desktop); + + [PreserveSig] + int MoveWindowToDesktop([In] IntPtr hTopLevelWindow, [MarshalAs(UnmanagedType.LPStruct)][In] Guid desktop); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs new file mode 100644 index 0000000000..e60cb262fe --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs @@ -0,0 +1,1161 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Text; + +using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; + +#pragma warning disable SA1649, CA1051, CA1707, CA1028, CA1714, CA1069, SA1402 + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")] +public static class NativeMethods +{ + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int EnumWindows(EnumWindowsProc callPtr, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr GetWindow(IntPtr hWnd, GetWindowCmd uCmd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int EnumChildWindows(IntPtr hWnd, EnumWindowsProc callPtr, int lPar); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); + + [DllImport("user32.dll")] + public static extern bool FlashWindow(IntPtr hwnd, bool bInvert); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("psapi.dll", BestFitMapping = false, CharSet = CharSet.Unicode)] + public static extern uint GetProcessImageFileName(IntPtr hProcess, [Out] StringBuilder lpImageFileName, [In][MarshalAs(UnmanagedType.U4)] int nSize); + + [DllImport("user32.dll", SetLastError = true, BestFitMapping = false, CharSet = CharSet.Unicode)] + public static extern IntPtr GetProp(IntPtr hWnd, string lpString); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId); + + [DllImport("dwmapi.dll", EntryPoint = "#113", CallingConvention = CallingConvention.StdCall)] + public static extern int DwmpActivateLivePreview([MarshalAs(UnmanagedType.Bool)] bool fActivate, IntPtr hWndExclude, IntPtr hWndInsertBefore, LivePreviewTrigger lpt, IntPtr prcFinalRect); + + [DllImport("dwmapi.dll", PreserveSig = false)] + public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref uint attrValue, int attrSize); + + [DllImport("dwmapi.dll", PreserveSig = false)] + public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute); + + [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Unicode)] + public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int msg, int wParam); + + [DllImport("user32.dll")] + public static extern int SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, int fuFlags, int uTimeout, out int lpdwResult); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("user32.dll")] + public static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumThreadWindows(uint threadId, ShellCommand.EnumThreadDelegate lpfn, IntPtr lParam); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetFirmwareType(ref FirmwareType FirmwareType); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ExitWindowsEx(uint uFlags, uint dwReason); + + [DllImport("user32")] + public static extern void LockWorkStation(); + + [DllImport("Powrprof.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); + + [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] + public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags); + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] + public static extern HRESULT SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, IntPtr ppvReserved); + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] + public static extern HRESULT SHCreateStreamOnFileEx(string fileName, STGM grfMode, uint attributes, bool create, System.Runtime.InteropServices.ComTypes.IStream reserved, out System.Runtime.InteropServices.ComTypes.IStream stream); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string path, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem); + + [DllImport("rpcrt4.dll")] + public static extern int UuidCreateSequential(out GUIDDATA Uuid); +} + +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "These are the names used by win32.")] +public static class Win32Constants +{ + /// + /// GetWindowLong index to retrieves the extended window styles. + /// + public const int GWL_EXSTYLE = -20; + + /// + /// A window receives this message when the user chooses a command from the Window menu (formerly known as the system or control menu) + /// or when the user chooses the maximize button, minimize button, restore button, or close button. + /// + public const int WM_SYSCOMMAND = 0x0112; + + /// + /// Restores the window to its normal position and size. + /// + public const int SC_RESTORE = 0xf120; + + /// + /// Closes the window + /// + public const int SC_CLOSE = 0xF060; + + /// + /// RPC call succeeded + /// + public const int RPC_S_OK = 0; + + /// + /// The UUID is guaranteed to be unique to this computer only. + /// + public const int RPC_S_UUID_LOCAL_ONLY = 0x720; +} + +public static class ShellItemTypeConstants +{ + /// + /// Guid for type IShellItem. + /// + public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe"); + + /// + /// Guid for type IShellItem2. + /// + public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93"); +} + +public enum HRESULT : uint +{ + /// + /// Operation successful. + /// + S_OK = 0x00000000, + + /// + /// Operation successful. (negative condition/no operation) + /// + S_FALSE = 0x00000001, + + /// + /// Not implemented. + /// + E_NOTIMPL = 0x80004001, + + /// + /// No such interface supported. + /// + E_NOINTERFACE = 0x80004002, + + /// + /// Pointer that is not valid. + /// + E_POINTER = 0x80004003, + + /// + /// Operation aborted. + /// + E_ABORT = 0x80004004, + + /// + /// Unspecified failure. + /// + E_FAIL = 0x80004005, + + /// + /// Unexpected failure. + /// + E_UNEXPECTED = 0x8000FFFF, + + /// + /// General access denied error. + /// + E_ACCESSDENIED = 0x80070005, + + /// + /// Handle that is not valid. + /// + E_HANDLE = 0x80070006, + + /// + /// Failed to allocate necessary memory. + /// + E_OUTOFMEMORY = 0x8007000E, + + /// + /// One or more arguments are not valid. + /// + E_INVALIDARG = 0x80070057, + + /// + /// The operation was canceled by the user. (Error source 7 means Win32.) + /// + /// + /// + E_CANCELLED = 0x800704C7, +} + +/// +/// see learn.microsoft.com +/// +public enum FirmwareType +{ + Unknown = 0, + Bios = 1, + Uefi = 2, + Max = 3, +} + +/// +/// see all STGM values +/// +[Flags] +public enum STGM : long +{ + READ = 0x00000000L, + WRITE = 0x00000001L, + READWRITE = 0x00000002L, + CREATE = 0x00001000L, +} + +public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + +/// +/// Some flags for interop calls to SetWindowPosition +/// +[Flags] +public enum SetWindowPosFlags : uint +{ + /// + /// If the calling thread and the thread that owns the window are attached to different input queues, the system posts the request to the thread that owns the window. This prevents the calling thread from blocking its execution while other threads process the request. + /// + SWP_ASYNCWINDOWPOS = 0x4000, + + /// + /// Prevents generation of the WM_SYNCPAINT message. + /// + SWP_DEFERERASE = 0x2000, + + /// + /// Draws a frame (defined in the window's class description) around the window. + /// + SWP_DRAWFRAME = 0x0020, + + /// + /// Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE message to the window, even if the window's size is not being changed. If this flag is not specified, WM_NCCALCSIZE is sent only when the window's size is being changed. + /// + SWP_FRAMECHANGED = 0x0020, + + /// + /// Hides the window. + /// + SWP_HIDEWINDOW = 0x0080, + + /// + /// Does not activate the window. If this flag is not set, the window is activated and moved to the top of either the topmost or non-topmost group (depending on the setting of the hWndInsertAfter parameter). + /// + SWP_NOACTIVATE = 0x0010, + + /// + /// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client area are saved and copied back into the client area after the window is sized or repositioned. + /// + SWP_NOCOPYBITS = 0x0100, + + /// + /// Retains the current position (ignores X and Y parameters). + /// + SWP_NOMOVE = 0x0002, + + /// + /// Does not change the owner window's position in the Z order. + /// + SWP_NOOWNERZORDER = 0x0200, + + /// + /// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area, the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any parts of the window and parent window that need redrawing. + /// + SWP_NOREDRAW = 0x0008, + + /// + /// Same as the SWP_NOOWNERZORDER flag. + /// + SWP_NOREPOSITION = 0x0200, + + /// + /// Prevents the window from receiving the WM_WINDOWPOSCHANGING message. + /// + SWP_NOSENDCHANGING = 0x0400, + + /// + /// Retains the current size (ignores the cx and cy parameters). + /// + SWP_NOSIZE = 0x0001, + + /// + /// Retains the current Z order (ignores the hWndInsertAfter parameter). + /// + SWP_NOZORDER = 0x0004, + + /// + /// Displays the window. + /// + SWP_SHOWWINDOW = 0x0040, +} + +/// +/// Options for DwmpActivateLivePreview +/// +public enum LivePreviewTrigger +{ + /// + /// Show Desktop button + /// + ShowDesktop = 1, + + /// + /// WIN+SPACE hotkey + /// + WinSpace, + + /// + /// Hover-over Superbar thumbnails + /// + Superbar, + + /// + /// Alt-Tab + /// + AltTab, + + /// + /// Press and hold on Superbar thumbnails + /// + SuperbarTouch, + + /// + /// Press and hold on Show desktop + /// + ShowDesktopTouch, +} + +/// +/// Show Window Enums +/// +public enum ShowWindowCommand +{ + /// + /// Hides the window and activates another window. + /// + Hide = 0, + + /// + /// Activates and displays a window. If the window is minimized or + /// maximized, the system restores it to its original size and position. + /// An application should specify this flag when displaying the window + /// for the first time. + /// + Normal = 1, + + /// + /// Activates the window and displays it as a minimized window. + /// + ShowMinimized = 2, + + /// + /// Maximizes the specified window. + /// + Maximize = 3, // is this the right value? + + /// + /// Activates the window and displays it as a maximized window. + /// + ShowMaximized = 3, + + /// + /// Displays a window in its most recent size and position. This value + /// is similar to , except + /// the window is not activated. + /// + ShowNoActivate = 4, + + /// + /// Activates the window and displays it in its current size and position. + /// + Show = 5, + + /// + /// Minimizes the specified window and activates the next top-level + /// window in the Z order. + /// + Minimize = 6, + + /// + /// Displays the window as a minimized window. This value is similar to + /// , except the + /// window is not activated. + /// + ShowMinNoActive = 7, + + /// + /// Displays the window in its current size and position. This value is + /// similar to , except the + /// window is not activated. + /// + ShowNA = 8, + + /// + /// Activates and displays the window. If the window is minimized or + /// maximized, the system restores it to its original size and position. + /// An application should specify this flag when restoring a minimized window. + /// + Restore = 9, + + /// + /// Sets the show state based on the SW_* value specified in the + /// STARTUPINFO structure passed to the CreateProcess function by the + /// program that started the application. + /// + ShowDefault = 10, + + /// + /// Windows 2000/XP: Minimizes a window, even if the thread + /// that owns the window is not responding. This flag should only be + /// used when minimizing windows from a different thread. + /// + ForceMinimize = 11, +} + +/// +/// The rendering policy to use for set window attribute +/// +[Flags] +public enum DwmNCRenderingPolicies +{ + UseWindowStyle, + Disabled, + Enabled, + Last, +} + +/// +/// DWM window attribute (Windows 7 and earlier: The values between ExcludedFromPeek and Last aren't supported.) +/// +[Flags] +public enum DwmWindowAttributes +{ + NCRenderingEnabled = 1, + NCRenderingPolicy = 2, + TransitionsForceDisabled = 3, + AllowNCPaint = 4, + CaptionButtonBounds = 5, + NonClientRtlLayout = 6, + ForceIconicRepresentation = 7, + Flip3DPolicy = 8, + ExtendedFrameBounds = 9, + HasIconicBitmap = 10, + DisallowPeek = 11, + ExcludedFromPeek = 12, + Cloak = 13, + Cloaked = 14, + FreezeRepresentation = 15, + PassiveUpdateMode = 16, + UseHostbackdropbrush = 17, + UseImmersiveDarkMode = 20, + WindowCornerPreference = 33, + BorderColor = 34, + CaptionColor = 35, + TextColor = 36, + VisibleFrameBorderThickness = 37, + Last, +} + +/// +/// Flags for describing the window cloak state (Windows 7 and earlier: This value is not supported.) +/// +[Flags] +public enum DwmWindowCloakStates +{ + None = 0, + CloakedApp = 1, + CloakedShell = 2, + CloakedInherited = 4, +} + +/// +/// Flags for accessing the process in trying to get icon for the process +/// +[Flags] +public enum ProcessAccessFlags +{ + /// + /// Required to create a thread. + /// + CreateThread = 0x0002, + + /// + /// Required to set the session id for a process. + /// + SetSessionId = 0x0004, + + /// + /// Required to perform an operation on the address space of a process + /// + VmOperation = 0x0008, + + /// + /// Required to read memory in a process using ReadProcessMemory. + /// + VmRead = 0x0010, + + /// + /// Required to write to memory in a process using WriteProcessMemory. + /// + VmWrite = 0x0020, + + /// + /// Required to duplicate a handle using DuplicateHandle. + /// + DupHandle = 0x0040, + + /// + /// Required to create a process. + /// + CreateProcess = 0x0080, + + /// + /// Required to set memory limits using SetProcessWorkingSetSize. + /// + SetQuota = 0x0100, + + /// + /// Required to set certain information about a process, such as its priority class (see SetPriorityClass). + /// + SetInformation = 0x0200, + + /// + /// Required to retrieve certain information about a process, such as its token, exit code, and priority class (see OpenProcessToken). + /// + QueryInformation = 0x0400, + + /// + /// Required to suspend or resume a process. + /// + SuspendResume = 0x0800, + + /// + /// Required to retrieve certain information about a process (see GetExitCodeProcess, GetPriorityClass, IsProcessInJob, QueryFullProcessImageName). + /// A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted PROCESS_QUERY_LIMITED_INFORMATION. + /// + QueryLimitedInformation = 0x1000, + + /// + /// Required to wait for the process to terminate using the wait functions. + /// + Synchronize = 0x100000, + + /// + /// Required to delete the object. + /// + Delete = 0x00010000, + + /// + /// Required to read information in the security descriptor for the object, not including the information in the SACL. + /// To read or write the SACL, you must request the ACCESS_SYSTEM_SECURITY access right. For more information, see SACL Access Right. + /// + ReadControl = 0x00020000, + + /// + /// Required to modify the DACL in the security descriptor for the object. + /// + WriteDac = 0x00040000, + + /// + /// Required to change the owner in the security descriptor for the object. + /// + WriteOwner = 0x00080000, + + /// + /// Combines , , , and . + /// + StandardRightsRequired = Delete | ReadControl | WriteDac | WriteOwner, // == 0x000F0000 + + /// + /// All possible access rights for a process object. + /// + AllAccess = StandardRightsRequired | Synchronize | 0xFFFF, +} + +[StructLayout(LayoutKind.Sequential)] +public struct GUIDDATA +{ + public int Data1; + public short Data2; + public short Data3; + [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] Data4; +} + +/// +/// Contains information about the placement of a window on the screen. +/// +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct WINDOWPLACEMENT : IEquatable +{ + /// + /// The length of the structure, in bytes. Before calling the GetWindowPlacement or SetWindowPlacement functions, set this member to sizeof(WINDOWPLACEMENT). + /// + /// GetWindowPlacement and SetWindowPlacement fail if this member is not set correctly. + /// + /// + public int Length; + + /// + /// Specifies flags that control the position of the minimized window and the method by which the window is restored. + /// + public int Flags; + + /// + /// The current show state of the window. + /// + public ShowWindowCommand ShowCmd; + + /// + /// The coordinates of the window's upper-left corner when the window is minimized. + /// + public POINT MinPosition; + + /// + /// The coordinates of the window's upper-left corner when the window is maximized. + /// + public POINT MaxPosition; + + /// + /// The window's coordinates when the window is in the restored position. + /// + public RECT NormalPosition; + + /// + /// Gets the default (empty) value. + /// + public static WINDOWPLACEMENT Default + { + get + { + WINDOWPLACEMENT result = default; + result.Length = Marshal.SizeOf(result); + return result; + } + } + + public static bool operator ==(WINDOWPLACEMENT left, WINDOWPLACEMENT right) + { + return left.Length == right.Length + && left.Flags == right.Flags + && left.ShowCmd == right.ShowCmd + && left.MinPosition == right.MinPosition + && left.MaxPosition == right.MaxPosition + && left.NormalPosition == right.NormalPosition; + } + + public static bool operator !=(WINDOWPLACEMENT left, WINDOWPLACEMENT right) + { + return !(left == right); + } + + public bool Equals(WINDOWPLACEMENT other) + { + return this == other; + } + + public override bool Equals(object? obj) + { + if (obj is WINDOWPLACEMENT wp) + { + return this == wp; + } + + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Length, Flags, ShowCmd, MinPosition, MaxPosition, NormalPosition); + } +} + +/// +/// Required pointless variables that we don't use in making a windows show +/// +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct RECT : IEquatable +{ + public int Left; + public int Top; + public int Right; + public int Bottom; + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + + public RECT(System.Drawing.Rectangle r) + : this(r.Left, r.Top, r.Right, r.Bottom) + { + } + + public int X + { + get => Left; + + set + { + Right -= Left - value; + Left = value; + } + } + + public int Y + { + get => Top; + + set + { + Bottom -= Top - value; + Top = value; + } + } + + public int Height + { + get => Bottom - Top; + set => Bottom = value + Top; + } + + public int Width + { + get => Right - Left; + set => Right = value + Left; + } + + public System.Drawing.Point Location + { + get => new(Left, Top); + set + { + X = value.X; + Y = value.Y; + } + } + + public System.Drawing.Size Size + { + get => new(Width, Height); + set + { + Width = value.Width; + Height = value.Height; + } + } + + public static implicit operator System.Drawing.Rectangle(RECT r) + { + return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height); + } + + public static implicit operator RECT(System.Drawing.Rectangle r) + { + return new RECT(r); + } + + public static bool operator ==(RECT r1, RECT r2) + { + return r1.Equals(r2); + } + + public static bool operator !=(RECT r1, RECT r2) + { + return !r1.Equals(r2); + } + + public bool Equals(RECT other) + { + return other.Left == Left && other.Top == Top && other.Right == Right && other.Bottom == Bottom; + } + + public override bool Equals(object? obj) + { + if (obj is RECT rect) + { + return Equals(rect); + } + + if (obj is System.Drawing.Rectangle rectangle) + { + return Equals(new RECT(rectangle)); + } + + return false; + } + + public override int GetHashCode() + { + return ((System.Drawing.Rectangle)this).GetHashCode(); + } + + public override string ToString() + { + // Using CurrentCulture since this is user facing + return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom); + } +} + +/// +/// Same as the RECT struct above +/// +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct POINT : IEquatable +{ + public int X; + public int Y; + + public POINT(int x, int y) + { + X = x; + Y = y; + } + + public POINT(System.Drawing.Point pt) + : this(pt.X, pt.Y) + { + } + + public static implicit operator System.Drawing.Point(POINT p) + { + return new System.Drawing.Point(p.X, p.Y); + } + + public static implicit operator POINT(System.Drawing.Point p) + { + return new POINT(p.X, p.Y); + } + + public override bool Equals(object? obj) + { + if (obj is POINT pt) + { + return this.X == pt.X && this.Y == pt.X; + } + + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(X, Y); + } + + public static bool operator ==(POINT left, POINT right) + { + return left.X == right.X && left.Y == right.Y; + } + + public static bool operator !=(POINT left, POINT right) + { + return !(left == right); + } + + public bool Equals(POINT other) + { + return this == other; + } +} + +/// +/// GetWindow relationship between the specified window and the window whose handle is to be retrieved. +/// +public enum GetWindowCmd : uint +{ + /// + /// The retrieved handle identifies the window of the same type that is highest in the Z order. + /// + GW_HWNDFIRST = 0, + + /// + /// The retrieved handle identifies the window of the same type that is lowest in the Z order. + /// + GW_HWNDLAST = 1, + + /// + /// The retrieved handle identifies the window below the specified window in the Z order. + /// + GW_HWNDNEXT = 2, + + /// + /// The retrieved handle identifies the window above the specified window in the Z order. + /// + GW_HWNDPREV = 3, + + /// + /// The retrieved handle identifies the specified window's owner window, if any. + /// + GW_OWNER = 4, + + /// + /// The retrieved handle identifies the child window at the top of the Z order, if the specified window + /// is a parent window. + /// + GW_CHILD = 5, + + /// + /// The retrieved handle identifies the enabled popup window owned by the specified window. + /// + GW_ENABLEDPOPUP = 6, +} + +/// +/// The following are the extended window styles +/// +[Flags] +public enum ExtendedWindowStyles : uint +{ + /// + /// The window has a double border; the window can, optionally, be created with a title bar by specifying + /// the WS_CAPTION style in the dwStyle parameter. + /// + WS_EX_DLGMODALFRAME = 0X0001, + + /// + /// The child window created with this style does not send the WM_PARENTNOTIFY message to its parent window + /// when it is created or destroyed. + /// + WS_EX_NOPARENTNOTIFY = 0X0004, + + /// + /// The window should be placed above all non-topmost windows and should stay above all non-topmost windows + /// and should stay above them, even when the window is deactivated. + /// + WS_EX_TOPMOST = 0X0008, + + /// + /// The window accepts drag-drop files. + /// + WS_EX_ACCEPTFILES = 0x0010, + + /// + /// The window should not be painted until siblings beneath the window (that were created by the same thread) + /// have been painted. + /// + WS_EX_TRANSPARENT = 0x0020, + + /// + /// The window is a MDI child window. + /// + WS_EX_MDICHILD = 0x0040, + + /// + /// The window is intended to be used as a floating toolbar. A tool window has a title bar that is shorter + /// than a normal title bar, and the window title is drawn using a smaller font. A tool window does not + /// appear in the taskbar or in the dialog that appears when the user presses ALT+TAB. + /// + WS_EX_TOOLWINDOW = 0x0080, + + /// + /// The window has a border with a raised edge. + /// + WS_EX_WINDOWEDGE = 0x0100, + + /// + /// The window has a border with a sunken edge. + /// + WS_EX_CLIENTEDGE = 0x0200, + + /// + /// The title bar of the window includes a question mark. + /// + WS_EX_CONTEXTHELP = 0x0400, + + /// + /// The window has generic "right-aligned" properties. This depends on the window class. This style has + /// an effect only if the shell language supports reading-order alignment, otherwise is ignored. + /// + WS_EX_RIGHT = 0x1000, + + /// + /// The window has generic left-aligned properties. This is the default. + /// + WS_EX_LEFT = 0x0, + + /// + /// If the shell language supports reading-order alignment, the window text is displayed using right-to-left + /// reading-order properties. For other languages, the styles is ignored. + /// + WS_EX_RTLREADING = 0x2000, + + /// + /// The window text is displayed using left-to-right reading-order properties. This is the default. + /// + WS_EX_LTRREADING = 0x0, + + /// + /// If the shell language supports reading order alignment, the vertical scroll bar (if present) is to + /// the left of the client area. For other languages, the style is ignored. + /// + WS_EX_LEFTSCROLLBAR = 0x4000, + + /// + /// The vertical scroll bar (if present) is to the right of the client area. This is the default. + /// + WS_EX_RIGHTSCROLLBAR = 0x0, + + /// + /// The window itself contains child windows that should take part in dialog box, navigation. If this + /// style is specified, the dialog manager recurses into children of this window when performing + /// navigation operations such as handling the TAB key, an arrow key, or a keyboard mnemonic. + /// + WS_EX_CONTROLPARENT = 0x10000, + + /// + /// The window has a three-dimensional border style intended to be used for items that do not accept + /// user input. + /// + WS_EX_STATICEDGE = 0x20000, + + /// + /// Forces a top-level window onto the taskbar when the window is visible. + /// + WS_EX_APPWINDOW = 0x40000, + + /// + /// The window is an overlapped window. + /// + WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, + + /// + /// The window is palette window, which is a modeless dialog box that presents an array of commands. + /// + WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, + + /// + /// The window is a layered window. This style cannot be used if the window has a class style of either + /// CS_OWNDC or CS_CLASSDC. Only for top level window before Windows 8, and child windows from Windows 8. + /// + WS_EX_LAYERED = 0x80000, + + /// + /// The window does not pass its window layout to its child windows. + /// + WS_EX_NOINHERITLAYOUT = 0x100000, + + /// + /// If the shell language supports reading order alignment, the horizontal origin of the window is on the + /// right edge. Increasing horizontal values advance to the left. + /// + WS_EX_LAYOUTRTL = 0x400000, + + /// + /// Paints all descendants of a window in bottom-to-top painting order using double-buffering. + /// Bottom-to-top painting order allows a descendent window to have translucency (alpha) and + /// transparency (color-key) effects, but only if the descendent window also has the WS_EX_TRANSPARENT + /// bit set. Double-buffering allows the window and its descendents to be painted without flicker. + /// + WS_EX_COMPOSITED = 0x2000000, + + /// + /// A top-level window created with this style does not become the foreground window when the user + /// clicks it. The system does not bring this window to the foreground when the user minimizes or closes + /// the foreground window. + /// + WS_EX_NOACTIVATE = 0x8000000, +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] +public interface IShellItem +{ + void BindToHandler( + IntPtr pbc, + [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out IntPtr ppv); + + void GetParent(out IShellItem ppsi); + + void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + + void Compare(IShellItem psi, uint hint, out int piOrder); +} + +/// +/// The following are ShellItem DisplayName types. +/// +[Flags] +public enum SIGDN : uint +{ + NORMALDISPLAY = 0, + PARENTRELATIVEPARSING = 0x80018001, + PARENTRELATIVEFORADDRESSBAR = 0x8001c001, + DESKTOPABSOLUTEPARSING = 0x80028000, + PARENTRELATIVEEDITING = 0x80031001, + DESKTOPABSOLUTEEDITING = 0x8004c000, + FILESYSPATH = 0x80058000, + URL = 0x80068000, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/OSVersionHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/OSVersionHelper.cs new file mode 100644 index 0000000000..dc6c9afbae --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/OSVersionHelper.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +public static class OSVersionHelper +{ + public static bool IsWindows11() + { + return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000; + } + + public static bool IsGreaterThanWindows11_21H2() + { + return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build > 22000; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..6f541d28df --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "windowWalker"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static SettingsManager? instance; + + private readonly ToggleSetting _resultsFromVisibleDesktopOnly = new( + Namespaced(nameof(ResultsFromVisibleDesktopOnly)), + Resources.windowwalker_SettingResultsVisibleDesktop, + Resources.windowwalker_SettingResultsVisibleDesktop, + false); + + private readonly ToggleSetting _subtitleShowPid = new( + Namespaced(nameof(SubtitleShowPid)), + Resources.windowwalker_SettingTagPid, + Resources.windowwalker_SettingTagPid, + false); + + private readonly ToggleSetting _subtitleShowDesktopName = new( + Namespaced(nameof(SubtitleShowDesktopName)), + Resources.windowwalker_SettingTagDesktopName, + Resources.windowwalker_SettingSubtitleDesktopName_Description, + true); + + private readonly ToggleSetting _confirmKillProcess = new( + Namespaced(nameof(ConfirmKillProcess)), + Resources.windowwalker_SettingConfirmKillProcess, + Resources.windowwalker_SettingConfirmKillProcess, + true); + + private readonly ToggleSetting _killProcessTree = new( + Namespaced(nameof(KillProcessTree)), + Resources.windowwalker_SettingKillProcessTree, + Resources.windowwalker_SettingKillProcessTree_Description, + false); + + private readonly ToggleSetting _openAfterKillAndClose = new( + Namespaced(nameof(OpenAfterKillAndClose)), + Resources.windowwalker_SettingOpenAfterKillAndClose, + Resources.windowwalker_SettingOpenAfterKillAndClose_Description, + false); + + private readonly ToggleSetting _hideKillProcessOnElevatedProcesses = new( + Namespaced(nameof(HideKillProcessOnElevatedProcesses)), + Resources.windowwalker_SettingHideKillProcess, + Resources.windowwalker_SettingHideKillProcess, + false); + + private readonly ToggleSetting _hideExplorerSettingInfo = new( + Namespaced(nameof(HideExplorerSettingInfo)), + Resources.windowwalker_SettingExplorerSettingInfo, + Resources.windowwalker_SettingExplorerSettingInfo_Description, + true); + + private readonly ToggleSetting _inMruOrder = new( + Namespaced(nameof(InMruOrder)), + Resources.windowwalker_SettingInMruOrder, + Resources.windowwalker_SettingInMruOrder_Description, + true); + + public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value; + + public bool SubtitleShowPid => _subtitleShowPid.Value; + + public bool SubtitleShowDesktopName => _subtitleShowDesktopName.Value; + + public bool ConfirmKillProcess => _confirmKillProcess.Value; + + public bool KillProcessTree => _killProcessTree.Value; + + public bool OpenAfterKillAndClose => _openAfterKillAndClose.Value; + + public bool HideKillProcessOnElevatedProcesses => _hideKillProcessOnElevatedProcesses.Value; + + public bool HideExplorerSettingInfo => _hideExplorerSettingInfo.Value; + + public bool InMruOrder => _inMruOrder.Value; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_resultsFromVisibleDesktopOnly); + Settings.Add(_subtitleShowPid); + Settings.Add(_subtitleShowDesktopName); + Settings.Add(_confirmKillProcess); + Settings.Add(_killProcessTree); + Settings.Add(_openAfterKillAndClose); + Settings.Add(_hideKillProcessOnElevatedProcesses); + Settings.Add(_hideExplorerSettingInfo); + Settings.Add(_inMruOrder); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } + + internal static SettingsManager Instance + { + get + { + instance ??= new SettingsManager(); + return instance; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ShellCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ShellCommand.cs new file mode 100644 index 0000000000..a09dc78648 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ShellCommand.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +public static class ShellCommand +{ + public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); + + private static bool containsSecurityWindow; + + public static Process? RunAsDifferentUser(ProcessStartInfo processStartInfo) + { + ArgumentNullException.ThrowIfNull(processStartInfo); + + processStartInfo.Verb = "RunAsUser"; + var process = Process.Start(processStartInfo); + + containsSecurityWindow = false; + + // wait for windows to bring up the "Windows Security" dialog + while (!containsSecurityWindow) + { + CheckSecurityWindow(); + Thread.Sleep(25); + } + + // while this process contains a "Windows Security" dialog, stay open + while (containsSecurityWindow) + { + containsSecurityWindow = false; + CheckSecurityWindow(); + Thread.Sleep(50); + } + + return process; + } + + private static void CheckSecurityWindow() + { + ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads; + for (var i = 0; i < ptc.Count; i++) + { + NativeMethods.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); + } + } + + private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam) + { + if (GetWindowTitle(hwnd) == "Windows Security") + { + containsSecurityWindow = true; + } + + return true; + } + + private static string GetWindowTitle(IntPtr hwnd) + { + StringBuilder sb = new StringBuilder(NativeMethods.GetWindowTextLength(hwnd) + 1); + _ = NativeMethods.GetWindowText(hwnd, sb, sb.Capacity); + return sb.ToString(); + } + + public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "") + { + var info = new ProcessStartInfo + { + FileName = fileName, + WorkingDirectory = workingDirectory, + Arguments = arguments, + Verb = verb, + }; + + return info; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VDesktop.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VDesktop.cs new file mode 100644 index 0000000000..d6332ab31f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VDesktop.cs @@ -0,0 +1,76 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +/// +/// Class that represents a Virtual Desktop +/// +/// This class is named VDesktop to make clear it isn't an instance of the original Desktop class from Virtual Desktop Manager. +/// We can't use the original one, because therefore we must access private com interfaces. We aren't allowed to do this, because this is an official Microsoft project. +public class VDesktop +{ + /// + /// Gets or sets the guid of the desktop + /// + public Guid Id + { + get; set; + } + + /// + /// Gets or sets the name of the desktop + /// + public string? Name + { + get; set; + } + + /// + /// Gets or sets the number (position) of the desktop + /// + public int Number + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the desktop is currently visible to the user + /// + public bool IsVisible + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the desktop guid belongs to the generic "AllDesktops" view. + /// This view hold all windows that are pinned to all desktops. + /// + public bool IsAllDesktopsView + { + get; set; + } + + /// + /// Gets or sets a value indicating the position of a desktop in the list of all desktops + /// + public VirtualDesktopPosition Position + { + get; set; + } + + /// + /// Gets an empty instance of + /// + public static VDesktop Empty => new() + { + Id = Guid.Empty, + Name = string.Empty, + Number = 0, + IsVisible = true, // Setting this always to true to simulate a visible desktop + IsAllDesktopsView = false, + Position = VirtualDesktopPosition.NotApplicable, + }; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs new file mode 100644 index 0000000000..023bd8ed33 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/VirtualDesktopHelper.cs @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +/// +/// Helper class to work with Virtual Desktops. +/// This helper uses only public available and documented COM-Interfaces or information from registry. +/// +/// +/// To use this helper you have to create an instance of it and access the method via the helper instance. +/// We are only allowed to use public documented com interfaces. +/// +/// Documentation of IVirtualDesktopManager interface +/// CSharp example code for IVirtualDesktopManager +public class VirtualDesktopHelper +{ + /// + /// Are we running on Windows 11 + /// + private readonly bool _isWindowsEleven; + + /// + /// Instance of "Virtual Desktop Manager" + /// + private readonly IVirtualDesktopManager? _virtualDesktopManager; + + /// + /// Internal settings to enable automatic update of desktop list. + /// This will be off by default to avoid to many registry queries. + /// + private readonly bool _desktopListAutoUpdate; + + /// + /// List of all available Virtual Desktop in their real order + /// The order and list in the registry is always up to date + /// + private readonly List _availableDesktops = []; + + /// + /// Id of the current visible Desktop. + /// + private Guid _currentDesktop; + + private static readonly CompositeFormat VirtualDesktopHelperDesktop = System.Text.CompositeFormat.Parse(Properties.Resources.VirtualDesktopHelper_Desktop); + + /// + /// Initializes a new instance of the class. + /// + /// Setting to configure if the list of available desktops should update automatically or only when calling . Per default this is set to manual update (false) to have less registry queries. + public VirtualDesktopHelper(bool desktopListUpdate = false) + { + try + { + _virtualDesktopManager = (IVirtualDesktopManager)new CVirtualDesktopManager(); + } + catch (COMException ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Initialization of failed: An exception was thrown when creating the instance of COM interface . {ex} " }); + return; + } + + _isWindowsEleven = OSVersionHelper.IsWindows11(); + _desktopListAutoUpdate = desktopListUpdate; + UpdateDesktopList(); + } + + /// + /// Gets a value indicating whether the Virtual Desktop Manager is initialized successfully + /// + public bool VirtualDesktopManagerInitialized => _virtualDesktopManager != null; + + /// + /// Method to update the list of Virtual Desktops from Registry + /// The data in the registry are always up to date + /// + /// If we cannot read from registry, we set the list/guid to empty values. + public void UpdateDesktopList() + { + var userSessionId = Process.GetCurrentProcess().SessionId; + var registrySessionVirtualDesktops = $"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\SessionInfo\\{userSessionId}\\VirtualDesktops"; // Windows 10 + var registryExplorerVirtualDesktops = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops"; // Windows 11 + + // List of all desktops + using RegistryKey? virtualDesktopKey = Registry.CurrentUser.OpenSubKey(registryExplorerVirtualDesktops, false); + if (virtualDesktopKey != null) + { + var allDeskValue = (byte[]?)virtualDesktopKey.GetValue("VirtualDesktopIDs", null) ?? Array.Empty(); + if (allDeskValue != null) + { + // We clear only, if we can read from registry. Otherwise we keep the existing values. + _availableDesktops.Clear(); + + // Each guid has a length of 16 elements + var numberOfDesktops = allDeskValue.Length / 16; + for (var i = 0; i < numberOfDesktops; i++) + { + var guidArray = new byte[16]; + Array.ConstrainedCopy(allDeskValue, i * 16, guidArray, 0, 16); + _availableDesktops.Add(new Guid(guidArray)); + } + } + else + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.UpdateDesktopList() failed to read the list of existing desktops form registry." }); + } + } + + // Guid for current desktop + var virtualDesktopsKeyName = _isWindowsEleven ? registryExplorerVirtualDesktops : registrySessionVirtualDesktops; + using RegistryKey? virtualDesktopsKey = Registry.CurrentUser.OpenSubKey(virtualDesktopsKeyName, false); + if (virtualDesktopsKey != null) + { + var currentVirtualDesktopValue = virtualDesktopsKey.GetValue("CurrentVirtualDesktop", null); + if (currentVirtualDesktopValue != null) + { + _currentDesktop = new Guid((byte[])currentVirtualDesktopValue); + } + else + { + // The registry value is missing when the user hasn't switched the desktop at least one time before reading the registry. In this case we can set it to desktop one. + // We can only set it to desktop one, if we have at least one desktop in the desktops list. Otherwise we keep the existing value. + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.UpdateDesktopList() failed to read the id for the current desktop form registry." }); + _currentDesktop = _availableDesktops.Count >= 1 ? _availableDesktops[0] : _currentDesktop; + } + } + } + + /// + /// Returns an ordered list with the ids of all existing desktops. The list is ordered in the same way as the existing desktops. + /// + /// List of desktop ids or an empty list on failure. + public List GetDesktopIdList() + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + return _availableDesktops; + } + + /// + /// Returns an ordered list with of all existing desktops and their properties. The list is ordered in the same way as the existing desktops. + /// + /// List of desktops or an empty list on failure. + public List GetDesktopList() + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + List list = new List(); + foreach (Guid d in _availableDesktops) + { + list.Add(CreateVDesktopInstance(d)); + } + + return list; + } + + /// + /// Returns the count of existing desktops + /// + /// Number of existing desktops or zero on failure. + public int GetDesktopCount() + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + return _availableDesktops.Count; + } + + /// + /// Returns the id of the desktop that is currently visible to the user. + /// + /// The of the current desktop. Or on failure and if we don't know the current desktop. + public Guid GetCurrentDesktopId() + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + return _currentDesktop; + } + + /// + /// Returns an instance of for the desktop that is currently visible to the user. + /// + /// An instance of for the current desktop, or an empty instance of on failure. + public VDesktop GetCurrentDesktop() + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + return CreateVDesktopInstance(_currentDesktop); + } + + /// + /// Checks if a desktop is currently visible to the user. + /// + /// The guid of the desktop to check. + /// if the guid belongs to the currently visible desktop. if not or if we don't know the visible desktop. + public bool IsDesktopVisible(Guid desktop) + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + return _currentDesktop == desktop; + } + + /// + /// Returns the number (position) of a desktop. + /// + /// The guid of the desktop. + /// Number of the desktop, if found. Otherwise a value of zero. + public int GetDesktopNumber(Guid desktop) + { + if (_desktopListAutoUpdate) + { + UpdateDesktopList(); + } + + // Adding +1 because index starts with zero and humans start counting with one. + return _availableDesktops.IndexOf(desktop) + 1; + } + + /// + /// Returns the name of a desktop + /// + /// Guid of the desktop + /// Returns the name of the desktop or on failure. + public string GetDesktopName(Guid desktop) + { + if (desktop == Guid.Empty || !GetDesktopIdList().Contains(desktop)) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.GetDesktopName() failed. Parameter contains an invalid desktop guid ({desktop}) that doesn't belongs to an available desktop. Maybe the guid belongs to the generic 'AllDesktops' view." }); + return string.Empty; + } + + // If the desktop name was not changed by the user, it isn't saved to the registry. Then we need the default name for the desktop. + var defaultName = string.Format(System.Globalization.CultureInfo.InvariantCulture, VirtualDesktopHelperDesktop, GetDesktopNumber(desktop)); + + var registryPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops\\Desktops\\{" + desktop.ToString().ToUpper(System.Globalization.CultureInfo.InvariantCulture) + "}"; + using RegistryKey? deskSubKey = Registry.CurrentUser.OpenSubKey(registryPath, false); + var desktopName = deskSubKey?.GetValue("Name"); + + return (desktopName != null) ? (string)desktopName : defaultName; + } + + /// + /// Returns the position type for a desktop. + /// + /// Guid of the desktop. + /// Type of . On failure we return . + public VirtualDesktopPosition GetDesktopPositionType(Guid desktop) + { + var desktopNumber = GetDesktopNumber(desktop); + var desktopCount = GetDesktopCount(); + + if (desktopCount == 0 || desktop == Guid.Empty) + { + // On failure or empty guid + return VirtualDesktopPosition.NotApplicable; + } + else if (desktopNumber == 1) + { + return VirtualDesktopPosition.FirstDesktop; + } + else if (desktopNumber == desktopCount) + { + return VirtualDesktopPosition.LastDesktop; + } + else if (desktopNumber > 1 & desktopNumber < desktopCount) + { + return VirtualDesktopPosition.BetweenOtherDesktops; + } + else + { + // All desktops view or a guid that doesn't belong to an existing desktop + return VirtualDesktopPosition.NotApplicable; + } + } + + /// + /// Returns the desktop id for a window. + /// + /// Handle of the window. + /// The guid of the desktop, where the window is shown. + /// HResult of the called method as integer. + public int GetWindowDesktopId(IntPtr hWindow, out Guid desktopId) + { + if (_virtualDesktopManager == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktopId() failed: The instance of isn't available." }); + desktopId = Guid.Empty; + return unchecked((int)HRESULT.E_UNEXPECTED); + } + + return _virtualDesktopManager.GetWindowDesktopId(hWindow, out desktopId); + } + + /// + /// Returns an instance of for the desktop where the window is assigned to. + /// + /// Handle of the window. + /// An instance of for the desktop where the window is assigned to, or an empty instance of on failure. + public VDesktop GetWindowDesktop(IntPtr hWindow) + { + if (_virtualDesktopManager == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktop() failed: The instance of isn't available." }); + return CreateVDesktopInstance(Guid.Empty); + } + + var hr = _virtualDesktopManager.GetWindowDesktopId(hWindow, out Guid desktopId); + return (hr != (int)HRESULT.S_OK || desktopId == Guid.Empty) ? VDesktop.Empty : CreateVDesktopInstance(desktopId, hWindow); + } + + /// + /// Returns the desktop assignment type for a window. + /// + /// Handle of the window. + /// Optional the desktop id if known + /// Type of . + public VirtualDesktopAssignmentType GetWindowDesktopAssignmentType(IntPtr hWindow, Guid? desktop = null) + { + if (_virtualDesktopManager == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.GetWindowDesktopAssignmentType() failed: The instance of isn't available." }); + return VirtualDesktopAssignmentType.Unknown; + } + + _ = _virtualDesktopManager.IsWindowOnCurrentVirtualDesktop(hWindow, out var isOnCurrentDesktop); + Guid windowDesktopId = desktop ?? Guid.Empty; // Prepare variable in case we have no input parameter for desktop + var hResult = desktop is null ? GetWindowDesktopId(hWindow, out windowDesktopId) : 0; + + if (hResult != (int)HRESULT.S_OK) + { + return VirtualDesktopAssignmentType.Unknown; + } + else if (windowDesktopId == Guid.Empty) + { + return VirtualDesktopAssignmentType.NotAssigned; + } + else if (isOnCurrentDesktop == 1 && !GetDesktopIdList().Contains(windowDesktopId)) + { + // These windows are marked as visible on the current desktop, but the desktop id doesn't belongs to an existing desktop. + // In this case the desktop id belongs to the generic view 'AllDesktops'. + return VirtualDesktopAssignmentType.AllDesktops; + } + else if (isOnCurrentDesktop == 1) + { + return VirtualDesktopAssignmentType.CurrentDesktop; + } + else + { + return VirtualDesktopAssignmentType.OtherDesktop; + } + } + + /// + /// Returns a value indicating if the window is assigned to a currently visible desktop. + /// + /// Handle to the top level window. + /// Optional the desktop id if known + /// if the desktop with the window is visible or if the window is assigned to all desktops. if the desktop is not visible and on failure, + public bool IsWindowOnVisibleDesktop(IntPtr hWindow, Guid? desktop = null) + { + return GetWindowDesktopAssignmentType(hWindow, desktop) == VirtualDesktopAssignmentType.CurrentDesktop || GetWindowDesktopAssignmentType(hWindow, desktop) == VirtualDesktopAssignmentType.AllDesktops; + } + + /// + /// Returns a value indicating if the window is cloaked by VirtualDesktopManager. + /// (A cloaked window is not visible to the user. But the window is still composed by DWM.) + /// + /// Handle of the window. + /// Optional the desktop id if known + /// A value indicating if the window is cloaked by Virtual Desktop Manager, because it is moved to another desktop. + public bool IsWindowCloakedByVirtualDesktopManager(IntPtr hWindow, Guid? desktop = null) + { + // If a window is hidden because it is moved to another desktop, then DWM returns type "CloakedShell". If DWM returns another type the window is not cloaked by shell or VirtualDesktopManager. + _ = NativeMethods.DwmGetWindowAttribute(hWindow, (int)DwmWindowAttributes.Cloaked, out var dwmCloakedState, sizeof(uint)); + return GetWindowDesktopAssignmentType(hWindow, desktop) == VirtualDesktopAssignmentType.OtherDesktop && dwmCloakedState == (int)DwmWindowCloakStates.CloakedShell; + } + + /// + /// Moves the window to a specific desktop. + /// + /// Handle of the top level window. + /// Guid of the target desktop. + /// on success and on failure. + public bool MoveWindowToDesktop(IntPtr hWindow, in Guid desktopId) + { + if (_virtualDesktopManager == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.MoveWindowToDesktop() failed: The instance of isn't available." }); + return false; + } + + var hr = _virtualDesktopManager.MoveWindowToDesktop(hWindow, desktopId); + if (hr != (int)HRESULT.S_OK) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.MoveWindowToDesktop() failed: An exception was thrown when moving the window ({hWindow}) to another desktop ({desktopId})." }); + return false; + } + + return true; + } + + /// + /// Move a window one desktop left. + /// + /// Handle of the top level window. + /// on success and on failure. + public bool MoveWindowOneDesktopLeft(IntPtr hWindow) + { + var hr = GetWindowDesktopId(hWindow, out Guid windowDesktop); + if (hr != (int)HRESULT.S_OK) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopLeft() failed when moving the window ({hWindow}) one desktop left: Can't get current desktop of the window." }); + return false; + } + + if (GetDesktopIdList().Count == 0 || GetWindowDesktopAssignmentType(hWindow, windowDesktop) == VirtualDesktopAssignmentType.Unknown || GetWindowDesktopAssignmentType(hWindow, windowDesktop) == VirtualDesktopAssignmentType.NotAssigned) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopLeft() failed when moving the window ({hWindow}) one desktop left: We can't find the target desktop. This can happen if the desktop list is empty or if the window isn't assigned to a specific desktop." }); + return false; + } + + var windowDesktopNumber = GetDesktopIdList().IndexOf(windowDesktop); + if (windowDesktopNumber == 1) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopLeft() failed when moving the window ({hWindow}) one desktop left: The window is on the first desktop." }); + return false; + } + + Guid newDesktop = _availableDesktops[windowDesktopNumber - 1]; + return MoveWindowToDesktop(hWindow, newDesktop); + } + + /// + /// Move a window one desktop right. + /// + /// Handle of the top level window. + /// on success and on failure. + public bool MoveWindowOneDesktopRight(IntPtr hWindow) + { + var hr = GetWindowDesktopId(hWindow, out Guid windowDesktop); + if (hr != (int)HRESULT.S_OK) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopRight() failed when moving the window ({hWindow}) one desktop right: Can't get current desktop of the window." }); + return false; + } + + if (GetDesktopIdList().Count == 0 || GetWindowDesktopAssignmentType(hWindow, windowDesktop) == VirtualDesktopAssignmentType.Unknown || GetWindowDesktopAssignmentType(hWindow, windowDesktop) == VirtualDesktopAssignmentType.NotAssigned) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopRight() failed when moving the window ({hWindow}) one desktop right: We can't find the target desktop. This can happen if the desktop list is empty or if the window isn't assigned to a specific desktop." }); + return false; + } + + var windowDesktopNumber = GetDesktopIdList().IndexOf(windowDesktop); + if (windowDesktopNumber == GetDesktopCount()) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"VirtualDesktopHelper.MoveWindowOneDesktopRight() failed when moving the window ({hWindow}) one desktop right: The window is on the last desktop." }); + return false; + } + + Guid newDesktop = _availableDesktops[windowDesktopNumber + 1]; + return MoveWindowToDesktop(hWindow, newDesktop); + } + + /// + /// Returns an instance of for a Guid. + /// + /// Guid of the desktop. + /// Handle of the window shown on the desktop. If this parameter is set we can detect if it is the AllDesktops view. + /// A instance. If the parameter desktop is , we return an empty instance. + private VDesktop CreateVDesktopInstance(Guid desktop, IntPtr hWindow = default) + { + if (desktop == Guid.Empty) + { + return VDesktop.Empty; + } + + // Can be only detected if method is invoked with window handle parameter. + VirtualDesktopAssignmentType desktopType = (hWindow != default) ? GetWindowDesktopAssignmentType(hWindow, desktop) : VirtualDesktopAssignmentType.Unknown; + var isAllDesktops = (hWindow != default) && desktopType == VirtualDesktopAssignmentType.AllDesktops; + var isDesktopVisible = (hWindow != default) ? (isAllDesktops || desktopType == VirtualDesktopAssignmentType.CurrentDesktop) : IsDesktopVisible(desktop); + + return new VDesktop() + { + Id = desktop, + Name = isAllDesktops ? Resources.VirtualDesktopHelper_AllDesktops : GetDesktopName(desktop), + Number = GetDesktopNumber(desktop), + IsVisible = isDesktopVisible || isAllDesktops, + IsAllDesktopsView = isAllDesktops, + Position = GetDesktopPositionType(desktop), + }; + } +} + +/// +/// Enum to show in which way a window is assigned to a desktop +/// +public enum VirtualDesktopAssignmentType +{ + Unknown = -1, + NotAssigned = 0, + AllDesktops = 1, + CurrentDesktop = 2, + OtherDesktop = 3, +} + +/// +/// Enum to show the position of a desktop in the list of all desktops +/// +public enum VirtualDesktopPosition +{ + FirstDesktop, + BetweenOtherDesktops, + LastDesktop, + NotApplicable, // If not applicable or unknown +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/Win32Helpers.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/Win32Helpers.cs new file mode 100644 index 0000000000..682c7fdfc4 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/Win32Helpers.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers; + +public static class Win32Helpers +{ + /// + /// Detects the type of system firmware which is equal to the boot type by calling the method . + /// + /// Firmware type like Uefi or Bios. + public static FirmwareType GetSystemFirmwareType() + { + FirmwareType firmwareType = default; + _ = NativeMethods.GetFirmwareType(ref firmwareType); + return firmwareType; + } + + /// + /// Returns the last Win32 Error code thrown by a native method if enabled for this method. + /// + /// The error code as int value. + public static int GetLastError() + { + return Marshal.GetLastPInvokeError(); + } + + /// + /// Validate that the handle is not null and close it. + /// + /// Handle to close. + /// Zero if native method fails and nonzero if the native method succeeds. + public static bool CloseHandleIfNotNull(IntPtr handle) + { + if (handle == IntPtr.Zero) + { + // Return true if there is nothing to close. + return true; + } + + return NativeMethods.CloseHandle(handle); + } + + /// + /// Gets the description for an HRESULT error code. + /// + /// The HRESULT number + /// A string containing the description. + public static string MessageFromHResult(int hr) + { + return Marshal.GetExceptionForHR(hr)?.Message ?? string.Empty; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj new file mode 100644 index 0000000000..aee96aaab1 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj @@ -0,0 +1,34 @@ + + + + enable + Microsoft.CmdPal.Ext.WindowWalker + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs new file mode 100644 index 0000000000..ef061d900a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Pages; + +internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposable +{ + private System.Threading.CancellationTokenSource _cancellationTokenSource = new(); + + private bool _disposed; + + public WindowWalkerListPage() + { + Icon = new IconInfo("\ue8f9"); // SwitchApps + Name = Resources.windowwalker_name; + Id = "com.microsoft.cmdpal.windowwalker"; + PlaceholderText = Resources.windowwalker_PlaceholderText; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) => + RaiseItemsChanged(0); + + public List Query(string query) + { + ArgumentNullException.ThrowIfNull(query); + + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new System.Threading.CancellationTokenSource(); + + WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.UpdateDesktopList(); + OpenWindows.Instance.UpdateOpenWindowsList(_cancellationTokenSource.Token); + SearchController.Instance.UpdateSearchText(query); + var searchControllerResults = SearchController.Instance.SearchMatches; + + return ResultHelper.GetResultList(searchControllerResults, !string.IsNullOrEmpty(query)); + } + + public override IListItem[] GetItems() => Query(SearchText).ToArray(); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _cancellationTokenSource?.Dispose(); + _disposed = true; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..3edc393e4f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs @@ -0,0 +1,396 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.WindowWalker.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Switch to. + /// + public static string switch_to_command_title { + get { + return ResourceManager.GetString("switch_to_command_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On all Desktops. + /// + public static string VirtualDesktopHelper_AllDesktops { + get { + return ResourceManager.GetString("VirtualDesktopHelper_AllDesktops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Desktop {0}. + /// + public static string VirtualDesktopHelper_Desktop { + get { + return ResourceManager.GetString("VirtualDesktopHelper_Desktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch between open windows. + /// + public static string window_walker_top_level_command_title { + get { + return ResourceManager.GetString("window_walker_top_level_command_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close window. + /// + public static string windowwalker_Close { + get { + return ResourceManager.GetString("windowwalker_Close", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Desktop. + /// + public static string windowwalker_Desktop { + get { + return ResourceManager.GetString("windowwalker_Desktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Folder windows do not run in separate processes. (Click to open Explorer properties.). + /// + public static string windowwalker_ExplorerInfoSubTitle { + get { + return ResourceManager.GetString("windowwalker_ExplorerInfoSubTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Info: Killing the Explorer process isn't possible.. + /// + public static string windowwalker_ExplorerInfoTitle { + get { + return ResourceManager.GetString("windowwalker_ExplorerInfoTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kill process. + /// + public static string windowwalker_Kill { + get { + return ResourceManager.GetString("windowwalker_Kill", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your are going to kill the following process:. + /// + public static string windowwalker_KillMessage { + get { + return ResourceManager.GetString("windowwalker_KillMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue?. + /// + public static string windowwalker_KillMessageQuestion { + get { + return ResourceManager.GetString("windowwalker_KillMessageQuestion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kill process confirmation. + /// + public static string windowwalker_KillMessageTitle { + get { + return ResourceManager.GetString("windowwalker_KillMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Because this is an app process, all instances of the app will be killed. Continue?. + /// + public static string windowwalker_KillMessageUwp { + get { + return ResourceManager.GetString("windowwalker_KillMessageUwp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Window Walker. + /// + public static string windowwalker_name { + get { + return ResourceManager.GetString("windowwalker_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not Responding. + /// + public static string windowwalker_NotResponding { + get { + return ResourceManager.GetString("windowwalker_NotResponding", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No.. + /// + public static string windowwalker_Number { + get { + return ResourceManager.GetString("windowwalker_Number", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to pid. + /// + public static string windowwalker_pid { + get { + return ResourceManager.GetString("windowwalker_pid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search open windows.... + /// + public static string windowwalker_PlaceholderText { + get { + return ResourceManager.GetString("windowwalker_PlaceholderText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switches between open windows. + /// + public static string windowwalker_plugin_description { + get { + return ResourceManager.GetString("windowwalker_plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Process name. + /// + public static string windowwalker_Process { + get { + return ResourceManager.GetString("windowwalker_Process", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Process id. + /// + public static string windowwalker_ProcessId { + get { + return ResourceManager.GetString("windowwalker_ProcessId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Running. + /// + public static string windowwalker_Running { + get { + return ResourceManager.GetString("windowwalker_Running", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request confirmation when killing a process. + /// + public static string windowwalker_SettingConfirmKillProcess { + get { + return ResourceManager.GetString("windowwalker_SettingConfirmKillProcess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide Explorer process information. + /// + public static string windowwalker_SettingExplorerSettingInfo { + get { + return ResourceManager.GetString("windowwalker_SettingExplorerSettingInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is an optional message that informs users about explorer behavior. + /// + public static string windowwalker_SettingExplorerSettingInfo_Description { + get { + return ResourceManager.GetString("windowwalker_SettingExplorerSettingInfo_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide "kill process" button if additional permissions required. + /// + public static string windowwalker_SettingHideKillProcess { + get { + return ResourceManager.GetString("windowwalker_SettingHideKillProcess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show windows in most-recently-used order. + /// + public static string windowwalker_SettingInMruOrder { + get { + return ResourceManager.GetString("windowwalker_SettingInMruOrder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When disabled, windows will be sorted by title. + /// + public static string windowwalker_SettingInMruOrder_Description { + get { + return ResourceManager.GetString("windowwalker_SettingInMruOrder_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Kill process and its child processes. + /// + public static string windowwalker_SettingKillProcessTree { + get { + return ResourceManager.GetString("windowwalker_SettingKillProcessTree", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Be careful when activating this. Killing the whole process tree can lead to problematic application crashes.. + /// + public static string windowwalker_SettingKillProcessTree_Description { + get { + return ResourceManager.GetString("windowwalker_SettingKillProcessTree_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stay open after closing windows and killing processes. + /// + public static string windowwalker_SettingOpenAfterKillAndClose { + get { + return ResourceManager.GetString("windowwalker_SettingOpenAfterKillAndClose", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This feature won't work if the kill process confirmation is enabled.. + /// + public static string windowwalker_SettingOpenAfterKillAndClose_Description { + get { + return ResourceManager.GetString("windowwalker_SettingOpenAfterKillAndClose_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show only results from visible desktop. + /// + public static string windowwalker_SettingResultsVisibleDesktop { + get { + return ResourceManager.GetString("windowwalker_SettingResultsVisibleDesktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string windowwalker_settings_name { + get { + return ResourceManager.GetString("windowwalker_settings_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This information is only shown in subtitle and tool tip, if you have at least two desktops.. + /// + public static string windowwalker_SettingSubtitleDesktopName_Description { + get { + return ResourceManager.GetString("windowwalker_SettingSubtitleDesktopName_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show desktop as a tag on the list item. + /// + public static string windowwalker_SettingTagDesktopName { + get { + return ResourceManager.GetString("windowwalker_SettingTagDesktopName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show process id as a tag on the list item. + /// + public static string windowwalker_SettingTagPid { + get { + return ResourceManager.GetString("windowwalker_SettingTagPid", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx new file mode 100644 index 0000000000..12781a6a24 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Window Walker + + + Switches between open windows + + + Running + + + Request confirmation when killing a process + + + Hide Explorer process information + Explorer is here the program File Explorer + + + Hide "kill process" button if additional permissions required + + + Show only results from visible desktop + + + Show desktop as a tag on the list item + + + Show process id as a tag on the list item + + + Stay open after closing windows and killing processes + + + Desktop + + + No. + Short version of "Number" + + + Process name + + + Process id + + + Folder windows do not run in separate processes. (Click to open Explorer properties.) + Explorer is here the program File Explorer + + + Info: Killing the Explorer process isn't possible. + Explorer is here the program File Explorer + + + Close window + + + Kill process + + + Your are going to kill the following process: + + + Continue? + + + Kill process confirmation + + + Because this is an app process, all instances of the app will be killed. Continue? + + + Kill process and its child processes + + + This is an optional message that informs users about explorer behavior + + + Be careful when activating this. Killing the whole process tree can lead to problematic application crashes. + + + This feature won't work if the kill process confirmation is enabled. + + + This information is only shown in subtitle and tool tip, if you have at least two desktops. + + + Show windows in most-recently-used order + + + When disabled, windows will be sorted by title + + + Not Responding + + + Switch between open windows + + + Switch to + + + On all Desktops + + + Desktop {0} + + + Settings + + + pid + + + Search open windows... + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerCommandsProvider.cs new file mode 100644 index 0000000000..234c14844c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerCommandsProvider.cs @@ -0,0 +1,37 @@ +// 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.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Pages; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker; + +public partial class WindowWalkerCommandsProvider : CommandProvider +{ + private readonly CommandItem _windowWalkerPageItem; + + internal static readonly VirtualDesktopHelper VirtualDesktopHelperInstance = new(); + + public WindowWalkerCommandsProvider() + { + Id = "WindowWalker"; + DisplayName = Resources.windowwalker_name; + Icon = new IconInfo("\ue8f9"); // SwitchApps + Settings = SettingsManager.Instance.Settings; + + _windowWalkerPageItem = new CommandItem(new WindowWalkerListPage()) + { + Title = Resources.window_walker_top_level_command_title, + Subtitle = Resources.windowwalker_name, + MoreCommands = [ + new CommandContextItem(Settings.SettingsPage), + ], + }; + } + + public override ICommandItem[] TopLevelCommands() => [_windowWalkerPageItem]; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs new file mode 100644 index 0000000000..72e648387e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowWalker.Commands; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowWalker; + +internal sealed partial class WindowWalkerListItem : ListItem +{ + private readonly Window? _window; + + public Window? Window => _window; + + public WindowWalkerListItem(Window? window) + : base(new SwitchToWindowCommand(window)) + { + _window = window; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Action.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Action.cs new file mode 100644 index 0000000000..fa06243801 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Action.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WindowsServices; + +public enum Action +{ + Start, + Stop, + Restart, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/OpenServicesCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/OpenServicesCommand.cs new file mode 100644 index 0000000000..312a3fc33d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/OpenServicesCommand.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsServices.Helpers; +using Microsoft.CmdPal.Ext.WindowsServices.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsServices.Commands; + +internal sealed partial class OpenServicesCommand : InvokableCommand +{ + private readonly ServiceResult _serviceResult; + + internal OpenServicesCommand(ServiceResult serviceResult) + { + _serviceResult = serviceResult; + Name = Resources.wox_plugin_service_open_services; + Icon = new IconInfo("\xE8A7"); // OpenInNewWindow icon + } + + public override CommandResult Invoke() + { + Task.Run(() => ServiceHelper.OpenServices()); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/RestartServiceCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/RestartServiceCommand.cs new file mode 100644 index 0000000000..48b2a861ee --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/RestartServiceCommand.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsServices.Helpers; +using Microsoft.CmdPal.Ext.WindowsServices.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsServices.Commands; + +internal sealed partial class RestartServiceCommand : InvokableCommand +{ + private readonly ServiceResult _serviceResult; + + internal RestartServiceCommand(ServiceResult serviceResult) + { + _serviceResult = serviceResult; + Name = Resources.wox_plugin_service_restart; + Icon = new IconInfo("\xE72C"); // Refresh icon + } + + public override CommandResult Invoke() + { + Task.Run(() => ServiceHelper.ChangeStatus(_serviceResult, Action.Restart)); + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/ServiceCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/ServiceCommand.cs new file mode 100644 index 0000000000..29a59fafaa --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Commands/ServiceCommand.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsServices.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsServices.Commands; + +internal sealed partial class ServiceCommand : InvokableCommand +{ + private readonly ServiceResult _serviceResult; + private readonly Action _action; + + internal ServiceCommand(ServiceResult serviceResult, Action action) + { + _serviceResult = serviceResult; + _action = action; + Name = action.ToString(); + if (serviceResult.IsRunning) + { + Icon = new IconInfo("\xE71A"); // Stop icon + } + else + { + Icon = new IconInfo("\xEDB5"); // Playbadge12 icon + } + } + + public override CommandResult Invoke() + { + Task.Run(() => ServiceHelper.ChangeStatus(_serviceResult, _action)); + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs new file mode 100644 index 0000000000..2a0147d20b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs @@ -0,0 +1,243 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.ServiceProcess; +using Microsoft.CmdPal.Ext.WindowsServices.Commands; +using Microsoft.CmdPal.Ext.WindowsServices.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.WindowsServices.Helpers; + +public static class ServiceHelper +{ + public static IEnumerable Search(string search) + { + var services = ServiceController.GetServices().OrderBy(s => s.DisplayName); + IEnumerable serviceList = []; + + if (search.StartsWith(Resources.wox_plugin_service_status + ":", StringComparison.CurrentCultureIgnoreCase)) + { + // allows queries like 'status:running' + serviceList = services.Where(s => GetLocalizedStatus(s.Status).Contains(search.Split(':')[1], StringComparison.CurrentCultureIgnoreCase)); + } + else if (search.StartsWith(Resources.wox_plugin_service_startup + ":", StringComparison.CurrentCultureIgnoreCase)) + { + // allows queries like 'startup:automatic' + serviceList = services.Where(s => GetLocalizedStartType(s.StartType, s.ServiceName).Contains(search.Split(':')[1], StringComparison.CurrentCultureIgnoreCase)); + } + else + { + // To show 'starts with' results first, we split the search into two steps and then concatenating the lists. + var servicesStartsWith = services + .Where(s => s.DisplayName.StartsWith(search, StringComparison.OrdinalIgnoreCase) || s.ServiceName.StartsWith(search, StringComparison.OrdinalIgnoreCase)); + var servicesContains = services.Except(servicesStartsWith) + .Where(s => s.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) || s.ServiceName.Contains(search, StringComparison.OrdinalIgnoreCase)); + serviceList = servicesStartsWith.Concat(servicesContains); + } + + return serviceList.Select(s => + { + var serviceResult = new ServiceResult(s); + ServiceCommand serviceCommand; + CommandContextItem[] moreCommands; + if (serviceResult.IsRunning) + { + serviceCommand = new ServiceCommand(serviceResult, Action.Stop); + moreCommands = [ + new CommandContextItem(new RestartServiceCommand(serviceResult)), + new CommandContextItem(new OpenServicesCommand(serviceResult)), + ]; + } + else + { + serviceCommand = new ServiceCommand(serviceResult, Action.Start); + moreCommands = [ + new CommandContextItem(new OpenServicesCommand(serviceResult)), + ]; + } + + IconInfo icon = new("\U0001f7e2"); // unicode LARGE GREEN CIRCLE + switch (s.Status) + { + case ServiceControllerStatus.Stopped: + icon = new("\U0001F534"); // unicode LARGE RED CIRCLE + break; + case ServiceControllerStatus.Running: + break; + case ServiceControllerStatus.Paused: + icon = new("\u23F8"); // unicode DOUBLE VERTICAL BAR, aka, "Pause" + break; + } + + return new ListItem(serviceCommand) + { + Title = s.DisplayName, + Subtitle = ServiceHelper.GetResultSubTitle(s), + MoreCommands = moreCommands, + Icon = icon, + + // TODO GH #78 we need to improve the icon story + // TODO GH #126 investigate tooltip story + // ToolTipData = new ToolTipData(serviceResult.DisplayName, serviceResult.ServiceName), + // IcoPath = icoPath, + }; + }); + } + + // TODO GH #118 IPublicAPI contextAPI isn't used anymore, but we need equivalent ways to show notifications and status + public static void ChangeStatus(ServiceResult serviceResult, Action action) + { + ArgumentNullException.ThrowIfNull(serviceResult); + + // ArgumentNullException.ThrowIfNull(contextAPI); + try + { + var info = new ProcessStartInfo + { + FileName = "net", + Verb = "runas", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + + if (action == Action.Start) + { + info.Arguments = $"start \"{serviceResult.ServiceName}\""; + } + else if (action == Action.Stop) + { + info.Arguments = $"stop \"{serviceResult.ServiceName}\""; + } + else if (action == Action.Restart) + { + info.FileName = "cmd"; + info.Arguments = $"/c net stop \"{serviceResult.ServiceName}\" && net start \"{serviceResult.ServiceName}\""; + } + + var process = Process.Start(info); + process.WaitForExit(); + var exitCode = process.ExitCode; + +#pragma warning disable IDE0059, CS0168, SA1005 + if (exitCode == 0) + { + // TODO GH #118 feedback to users + // contextAPI.ShowNotification(GetLocalizedMessage(action), serviceResult.DisplayName); + } + else + { + // TODO GH #108 We need to figure out some logging + // contextAPI.ShowNotification(GetLocalizedErrorMessage(action), serviceResult.DisplayName); + // Log.Error($"The command returned {exitCode}", MethodBase.GetCurrentMethod().DeclaringType); + } + } + catch (Win32Exception ex) + { + // TODO GH #108 We need to figure out some logging + // Log.Error(ex.Message, MethodBase.GetCurrentMethod().DeclaringType); + } + } +#pragma warning restore IDE0059, CS0168, SA1005 + + public static void OpenServices() + { + try + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "services.msc", + UseShellExecute = true, + }; + + System.Diagnostics.Process.Start(startInfo); + } +#pragma warning disable IDE0059, CS0168, SA1005 + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + } + } +#pragma warning restore IDE0059, CS0168, SA1005 + + private static string GetResultSubTitle(ServiceController serviceController) + { + ArgumentNullException.ThrowIfNull(serviceController); + return $"{Resources.wox_plugin_service_status}: {GetLocalizedStatus(serviceController.Status)} - {Resources.wox_plugin_service_startup}: {GetLocalizedStartType(serviceController.StartType, serviceController.ServiceName)} - {Resources.wox_plugin_service_name}: {serviceController.ServiceName}"; + } + + private static string GetLocalizedStatus(ServiceControllerStatus status) + { + if (status == ServiceControllerStatus.Stopped) + { + return Resources.wox_plugin_service_stopped; + } + else if (status == ServiceControllerStatus.StartPending) + { + return Resources.wox_plugin_service_start_pending; + } + else if (status == ServiceControllerStatus.StopPending) + { + return Resources.wox_plugin_service_stop_pending; + } + else if (status == ServiceControllerStatus.Running) + { + return Resources.wox_plugin_service_running; + } + else + { + return status == ServiceControllerStatus.ContinuePending + ? Resources.wox_plugin_service_continue_pending + : status == ServiceControllerStatus.PausePending + ? Resources.wox_plugin_service_pause_pending + : status == ServiceControllerStatus.Paused ? Resources.wox_plugin_service_paused : status.ToString(); + } + } + + private static string GetLocalizedStartType(ServiceStartMode startMode, string serviceName) + { + if (startMode == ServiceStartMode.Boot) + { + return Resources.wox_plugin_service_start_mode_boot; + } + else if (startMode == ServiceStartMode.System) + { + return Resources.wox_plugin_service_start_mode_system; + } + else + { + return startMode == ServiceStartMode.Automatic + ? !IsDelayedStart(serviceName) ? Resources.wox_plugin_service_start_mode_automatic : Resources.wox_plugin_service_start_mode_automaticDelayed + : startMode == ServiceStartMode.Manual + ? Resources.wox_plugin_service_start_mode_manual + : startMode == ServiceStartMode.Disabled ? Resources.wox_plugin_service_start_mode_disabled : startMode.ToString(); + } + } + + private static string GetLocalizedMessage(Action action) + { + return action == Action.Start + ? Resources.wox_plugin_service_started_notification + : action == Action.Stop + ? Resources.wox_plugin_service_stopped_notification + : action == Action.Restart ? Resources.wox_plugin_service_restarted_notification : string.Empty; + } + + private static string GetLocalizedErrorMessage(Action action) + { + return action == Action.Start + ? Resources.wox_plugin_service_start_error_notification + : action == Action.Stop + ? Resources.wox_plugin_service_stop_error_notification + : action == Action.Restart ? Resources.wox_plugin_service_restart_error_notification : string.Empty; + } + + private static bool IsDelayedStart(string serviceName) => (int?)Registry.LocalMachine.OpenSubKey(@"System\CurrentControlSet\Services\" + serviceName, false)?.GetValue("DelayedAutostart", 0, RegistryValueOptions.None) == 1; +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj new file mode 100644 index 0000000000..2b37de7635 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj @@ -0,0 +1,31 @@ + + + + Microsoft.CmdPal.Ext.WindowsServices + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.WindowsServices.pri + + + + + + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs new file mode 100644 index 0000000000..1d7b4d43f1 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs @@ -0,0 +1,29 @@ +// 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.Runtime.InteropServices.WindowsRuntime; +using Microsoft.CmdPal.Ext.WindowsServices.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsServices; + +internal sealed partial class ServicesListPage : DynamicListPage +{ + public ServicesListPage() + { + Icon = WindowsServicesCommandsProvider.ServicesIcon; + Name = "Windows Services"; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0); + + public override IListItem[] GetItems() + { + var items = ServiceHelper.Search(SearchText).ToArray(); + + return items; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..04afe59f7a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.Designer.cs @@ -0,0 +1,333 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.WindowsServices.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.WindowsServices.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string test_value { + get { + return ResourceManager.GetString("test_value", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue. + /// + internal static string wox_plugin_service_continue_pending { + get { + return ResourceManager.GetString("wox_plugin_service_continue_pending", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + internal static string wox_plugin_service_name { + get { + return ResourceManager.GetString("wox_plugin_service_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open services (Ctrl+O). + /// + internal static string wox_plugin_service_open_services { + get { + return ResourceManager.GetString("wox_plugin_service_open_services", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pausing. + /// + internal static string wox_plugin_service_pause_pending { + get { + return ResourceManager.GetString("wox_plugin_service_pause_pending", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Paused. + /// + internal static string wox_plugin_service_paused { + get { + return ResourceManager.GetString("wox_plugin_service_paused", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manages Windows services. + /// + internal static string wox_plugin_service_plugin_description { + get { + return ResourceManager.GetString("wox_plugin_service_plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Service. + /// + internal static string wox_plugin_service_plugin_name { + get { + return ResourceManager.GetString("wox_plugin_service_plugin_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart (Ctrl+R). + /// + internal static string wox_plugin_service_restart { + get { + return ResourceManager.GetString("wox_plugin_service_restart", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while restarting the service. + /// + internal static string wox_plugin_service_restart_error_notification { + get { + return ResourceManager.GetString("wox_plugin_service_restart_error_notification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The service has been restarted. + /// + internal static string wox_plugin_service_restarted_notification { + get { + return ResourceManager.GetString("wox_plugin_service_restarted_notification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Running. + /// + internal static string wox_plugin_service_running { + get { + return ResourceManager.GetString("wox_plugin_service_running", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start (Enter). + /// + internal static string wox_plugin_service_start { + get { + return ResourceManager.GetString("wox_plugin_service_start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while starting the service. + /// + internal static string wox_plugin_service_start_error_notification { + get { + return ResourceManager.GetString("wox_plugin_service_start_error_notification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatic. + /// + internal static string wox_plugin_service_start_mode_automatic { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_automatic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatic (Delayed Start). + /// + internal static string wox_plugin_service_start_mode_automaticDelayed { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_automaticDelayed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Boot. + /// + internal static string wox_plugin_service_start_mode_boot { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_boot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disabled. + /// + internal static string wox_plugin_service_start_mode_disabled { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_disabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manual. + /// + internal static string wox_plugin_service_start_mode_manual { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_manual", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System. + /// + internal static string wox_plugin_service_start_mode_system { + get { + return ResourceManager.GetString("wox_plugin_service_start_mode_system", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starting. + /// + internal static string wox_plugin_service_start_pending { + get { + return ResourceManager.GetString("wox_plugin_service_start_pending", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Started. + /// + internal static string wox_plugin_service_started { + get { + return ResourceManager.GetString("wox_plugin_service_started", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The service has been started. + /// + internal static string wox_plugin_service_started_notification { + get { + return ResourceManager.GetString("wox_plugin_service_started_notification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Startup. + /// + internal static string wox_plugin_service_startup { + get { + return ResourceManager.GetString("wox_plugin_service_startup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Status. + /// + internal static string wox_plugin_service_status { + get { + return ResourceManager.GetString("wox_plugin_service_status", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop (Enter). + /// + internal static string wox_plugin_service_stop { + get { + return ResourceManager.GetString("wox_plugin_service_stop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while stopping the service. + /// + internal static string wox_plugin_service_stop_error_notification { + get { + return ResourceManager.GetString("wox_plugin_service_stop_error_notification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stopping. + /// + internal static string wox_plugin_service_stop_pending { + get { + return ResourceManager.GetString("wox_plugin_service_stop_pending", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stopped. + /// + internal static string wox_plugin_service_stopped { + get { + return ResourceManager.GetString("wox_plugin_service_stopped", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The service has been stopped. + /// + internal static string wox_plugin_service_stopped_notification { + get { + return ResourceManager.GetString("wox_plugin_service_stopped_notification", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.resx new file mode 100644 index 0000000000..0c789b6799 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/Properties/Resources.resx @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Continue + + + Name + + + Open services (Ctrl+O) + + + Paused + + + Pausing + + + Manages Windows services + + + Service + + + Restart (Ctrl+R) + + + The service has been restarted + + + An error occurred while restarting the service + + + Running + + + Start (Enter) + + + Started + + + The service has been started + + + Startup + + + An error occurred while starting the service + + + Automatic + + + Automatic (Delayed Start) + + + Boot + + + Disabled + + + Manual + + + System + + + Starting + + + Status + + + Stop (Enter) + + + Stopped + + + The service has been stopped + + + An error occurred while stopping the service + + + Stopping + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/ServiceResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/ServiceResult.cs new file mode 100644 index 0000000000..279e55dffd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/ServiceResult.cs @@ -0,0 +1,29 @@ +// 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.ServiceProcess; + +namespace Microsoft.CmdPal.Ext.WindowsServices; + +public class ServiceResult +{ + public string ServiceName { get; } + + public string DisplayName { get; } + + public ServiceStartMode StartMode { get; } + + public bool IsRunning { get; } + + public ServiceResult(ServiceController serviceController) + { + ArgumentNullException.ThrowIfNull(serviceController); + + ServiceName = serviceController.ServiceName; + DisplayName = serviceController.DisplayName; + StartMode = serviceController.StartType; + IsRunning = serviceController.Status != ServiceControllerStatus.Stopped && serviceController.Status != ServiceControllerStatus.StopPending; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs new file mode 100644 index 0000000000..dcea76a1c6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsServices; + +public partial class WindowsServicesCommandsProvider : CommandProvider +{ + // For giggles, "%windir%\\system32\\filemgmt.dll" also _just works_. + public static IconInfo ServicesIcon { get; } = new("\ue9f5"); + + public WindowsServicesCommandsProvider() + { + Id = "Windows.Services"; + DisplayName = $"Windows Services"; + Icon = ServicesIcon; + } + + public override ICommandItem[] TopLevelCommands() + { + return [ + new CommandItem(new ServicesListPage()) + { + Title = "Windows Services", + Subtitle = "Manage Windows Services", + } + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.png new file mode 100644 index 0000000000..9bd3a1f509 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.svg new file mode 100644 index 0000000000..304a0aa25c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Assets/WindowsSettings.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs new file mode 100644 index 0000000000..fa6485d138 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSetting.cs @@ -0,0 +1,86 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Classes; + +/// +/// A windows setting +/// +internal sealed class WindowsSetting +{ + /// + /// Initializes a new instance of the class. + /// + public WindowsSetting() + { + Name = string.Empty; + Command = string.Empty; + Type = string.Empty; + ShowAsFirstResult = false; + } + + /// + /// Gets or sets the name of this setting. + /// + public string Name { get; set; } + + /// + /// Gets or sets the areas of this setting. The order is fixed to the order in json. + /// +#pragma warning disable CS8632 + public IList? Areas { get; set; } + + /// + /// Gets or sets the command of this setting. + /// + public string Command { get; set; } + + /// + /// Gets or sets the type of the windows setting. + /// + public string Type { get; set; } + + /// + /// Gets or sets the alternative names of this setting. + /// + public IEnumerable? AltNames { get; set; } + + /// + /// Gets or sets a additional note of this settings. + /// (e.g. why is not supported on your system) + /// + public string? Note { get; set; } + + /// + /// Gets or sets the minimum need Windows build for this setting. + /// + public uint? IntroducedInBuild { get; set; } + + /// + /// Gets or sets the Windows build since this settings is not longer present. + /// + public uint? DeprecatedInBuild { get; set; } + + /// + /// Gets or sets a value indicating whether to use a higher score as normal for this setting to show it as one of the first results. + /// + public bool ShowAsFirstResult { get; set; } + + /// + /// Gets or sets the value with the generated area path as string. + /// This Property IS NOT PART OF THE DATA IN "WindowsSettings.json". + /// This property will be filled on runtime by "WindowsSettingsPathHelper". + /// + public string? JoinedAreaPath { get; set; } + + /// + /// Gets or sets the value with the generated full settings path (App and areas) as string. + /// This Property IS NOT PART OF THE DATA IN "WindowsSettings.json". + /// This property will be filled on runtime by "WindowsSettingsPathHelper". + /// + public string? JoinedFullSettingsPath { get; set; } +#pragma warning restore CS8632 +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSettings.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSettings.cs new file mode 100644 index 0000000000..cc24c6dcc7 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Classes/WindowsSettings.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Classes; + +/// +/// A class that contain all possible windows settings +/// +internal sealed class WindowsSettings +{ + /// + /// Initializes a new instance of the class with an empty settings list. + /// + public WindowsSettings() + { + Settings = Enumerable.Empty(); + } + + /// + /// Gets or sets a list with all possible windows settings + /// + public IEnumerable Settings { get; set; } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/CopySettingCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/CopySettingCommand.cs new file mode 100644 index 0000000000..7f5fa1789e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/CopySettingCommand.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsSettings.Classes; +using Microsoft.CmdPal.Ext.WindowsSettings.Helpers; +using Microsoft.CmdPal.Ext.WindowsSettings.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.Networking.NetworkOperators; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Commands; + +internal sealed partial class CopySettingCommand : InvokableCommand +{ + private readonly WindowsSetting _entry; + + internal CopySettingCommand(WindowsSetting entry) + { + Name = Resources.CopyCommand; + Icon = new IconInfo("\xE8C8"); // Copy icon + _entry = entry; + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetText(_entry.Command); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/OpenSettingsCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/OpenSettingsCommand.cs new file mode 100644 index 0000000000..9460cd9240 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Commands/OpenSettingsCommand.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsSettings.Classes; +using Microsoft.CmdPal.Ext.WindowsSettings.Helpers; +using Microsoft.CmdPal.Ext.WindowsSettings.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using Windows.Networking.NetworkOperators; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Commands; + +internal sealed partial class OpenSettingsCommand : InvokableCommand +{ + private readonly WindowsSetting _entry; + + internal OpenSettingsCommand(WindowsSetting entry) + { + Name = Resources.OpenSettings; + Icon = new IconInfo("\xE8C8"); + _entry = entry; + } + + private static bool DoOpenSettingsAction(WindowsSetting entry) + { + ProcessStartInfo processStartInfo; + + var command = entry.Command; + + if (command.Contains("%windir%", StringComparison.InvariantCultureIgnoreCase)) + { + var windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + command = command.Replace("%windir%", windowsFolder, StringComparison.InvariantCultureIgnoreCase); + } + + if (command.Contains(' ')) + { + var commandSplit = command.Split(' '); + var file = commandSplit.FirstOrDefault() ?? string.Empty; + var arguments = command[file.Length..].TrimStart(); + + processStartInfo = new ProcessStartInfo(file, arguments) + { + UseShellExecute = false, + }; + } + else + { + processStartInfo = new ProcessStartInfo(command) + { + UseShellExecute = true, + }; + } + + try + { + Process.Start(processStartInfo); + return true; + } +#pragma warning disable CS0168, IDE0059 + catch (Exception exception) + { + // TODO GH #108 Logging is something we have to take care of + // Log.Exception("can't open settings", exception, typeof(ResultHelper)); + return false; + } +#pragma warning restore CS0168, IDE0059 + } + + public override CommandResult Invoke() + { + DoOpenSettingsAction(_entry); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ContextMenuHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ContextMenuHelper.cs new file mode 100644 index 0000000000..83a0d2eb8c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ContextMenuHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Input; + +using Microsoft.CmdPal.Ext.WindowsSettings.Classes; +using Microsoft.CmdPal.Ext.WindowsSettings.Commands; +using Microsoft.CmdPal.Ext.WindowsSettings.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers; + +/// +/// Helper class to easier work with context menu entries +/// +internal static class ContextMenuHelper +{ + internal static List GetContextMenu(WindowsSetting entry) + { + var list = new List(1) + { + new(new CopySettingCommand(entry)), + }; + + return list; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs new file mode 100644 index 0000000000..f99f0ce3b3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers; + +/// +/// Helper class to easier work with the JSON file that contains all Windows settings +/// +internal static class JsonSettingsListHelper +{ + /// + /// The name of the file that contains all settings for the query + /// + private const string _settingsFile = "WindowsSettings.json"; + + private static readonly JsonSerializerOptions _serializerOptions = new() + { + }; + + /// + /// Read all possible Windows settings. + /// + /// A list with all possible windows settings. + internal static Classes.WindowsSettings ReadAllPossibleSettings() + { + var assembly = Assembly.GetExecutingAssembly(); + var type = assembly.GetTypes().FirstOrDefault(x => x.Name == nameof(WindowsSettingsCommandsProvider)); + +#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + Classes.WindowsSettings? settings = null; +#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + + try + { + var resourceName = $"{type?.Namespace}.{_settingsFile}"; + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream is null) + { + throw new ArgumentNullException(nameof(stream), "stream is null"); + } + + var options = _serializerOptions; + options.Converters.Add(new JsonStringEnumConverter()); + + using var reader = new StreamReader(stream); + var text = reader.ReadToEnd(); + + settings = JsonSerializer.Deserialize(text, options); + } +#pragma warning disable CS0168 + catch (Exception exception) + { + // TODO GH #108 Logging is something we have to take care of + // Log.Exception("Error loading settings JSON file", exception, typeof(JsonSettingsListHelper)); + } +#pragma warning restore CS0168 + return settings ?? new Classes.WindowsSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ResultHelper.cs new file mode 100644 index 0000000000..70d6c48ac2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/ResultHelper.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; + +using Microsoft.CmdPal.Ext.WindowsSettings.Commands; +using Microsoft.CmdPal.Ext.WindowsSettings.Helpers; +using Microsoft.CmdPal.Ext.WindowsSettings.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsSettings; + +/// +/// Helper class to easier work with List Items +/// +internal static class ResultHelper +{ + internal static List GetResultList( + in IEnumerable list, + string query) + { + var resultList = new List(list.Count()); + + foreach (var entry in list) + { + var result = new ListItem(new OpenSettingsCommand(entry)) + { + Icon = IconHelpers.FromRelativePath("Assets\\WindowsSettings.svg"), + Subtitle = entry.JoinedFullSettingsPath, + Title = entry.Name, + MoreCommands = ContextMenuHelper.GetContextMenu(entry).ToArray(), + }; + + // TODO GH #126 investigate tooltips + // AddOptionalToolTip(entry, result); + + // There is a case with MMC snap-ins where we don't have .msc files fort them. Then we need to show the note for this results in subtitle too. + // These results have mmc.exe as command and their note property is filled. + if (entry.Command == "mmc.exe" && !string.IsNullOrEmpty(entry.Note)) + { + result.Subtitle += $"\u0020\u0020\u002D\u0020\u0020{Resources.Note}: {entry.Note}"; // "\u0020\u0020\u002D\u0020\u0020" = "" + } + + // To not show duplicate entries we check the existing results on the list before adding the new entry. Example: Device Manager entry for Control Panel and Device Manager entry for MMC. + if (!resultList.Any(x => x.Title == result.Title)) + { + resultList.Add(result); + } + } + + // TODO GH #127 --> Investigate scoring + + // SetScores(resultList, query); + return resultList; + } + + /// + /// Checks if a setting matches the search string to filter settings by settings path. + /// This method is called from the method in if the search string contains the character ">". + /// + /// The WindowsSetting's result that should be checked. + /// The searchString entered by the user s. + internal static bool FilterBySettingsPath(in Classes.WindowsSetting found, in string queryString) + { + if (!queryString.Contains('>')) + { + return false; + } + + // Init vars + var queryElements = queryString.Split('>'); + + List settingsPath = new List(); + settingsPath.Add(found.Type); + if (!(found.Areas is null)) + { + settingsPath.AddRange(found.Areas); + } + + // Compare query and settings path + for (var i = 0; i < queryElements.Length; i++) + { + if (string.IsNullOrWhiteSpace(queryElements[i])) + { + // The queryElement is an WhiteSpace. Nothing to compare. + break; + } + + if (i < settingsPath.Count) + { + if (!settingsPath[i].StartsWith(queryElements[i], StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + } + else + { + // The user has entered more query parts than existing elements in settings path. + return false; + } + } + + // Return "true" if matches . + return true; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/TranslationHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/TranslationHelper.cs new file mode 100644 index 0000000000..b5a8c29845 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/TranslationHelper.cs @@ -0,0 +1,117 @@ +// 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.Collections.ObjectModel; +using System.Globalization; +using System.Linq; + +using Microsoft.CmdPal.Ext.WindowsSettings.Properties; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers; + +/// +/// Helper class to easier work with translations. +/// +internal static class TranslationHelper +{ + /// + /// Translate all settings of the settings list in the given class. + /// + /// A class that contain all possible windows settings. + internal static void TranslateAllSettings(in Classes.WindowsSettings windowsSettings) + { + if (windowsSettings?.Settings is null) + { + return; + } + + foreach (var settings in windowsSettings.Settings) + { + // Translate Name + if (!string.IsNullOrWhiteSpace(settings.Name)) + { + var name = Resources.ResourceManager.GetString(settings.Name, CultureInfo.CurrentUICulture); + if (string.IsNullOrEmpty(name)) + { + // Log.Warn($"Resource string for [{settings.Name}] not found", typeof(TranslationHelper)); + } + + settings.Name = name ?? settings.Name ?? string.Empty; + } + + // Translate Type (App) + if (!string.IsNullOrWhiteSpace(settings.Type)) + { + var type = Resources.ResourceManager.GetString(settings.Type, CultureInfo.CurrentUICulture); + if (string.IsNullOrEmpty(type)) + { + // Log.Warn($"Resource string for [{settings.Type}] not found", typeof(TranslationHelper)); + } + + settings.Type = type ?? settings.Type ?? string.Empty; + } + + // Translate Areas + if (!(settings.Areas is null) && settings.Areas.Any()) + { + var translatedAreas = new List(); + + foreach (var area in settings.Areas) + { + if (string.IsNullOrWhiteSpace(area)) + { + continue; + } + + var translatedArea = Resources.ResourceManager.GetString(area, CultureInfo.CurrentUICulture); + if (string.IsNullOrEmpty(translatedArea)) + { + // Log.Warn($"Resource string for [{area}] not found", typeof(TranslationHelper)); + } + + translatedAreas.Add(translatedArea ?? area); + } + + settings.Areas = translatedAreas; + } + + // Translate Alternative names + if (!(settings.AltNames is null) && settings.AltNames.Any()) + { + var translatedAltNames = new Collection(); + + foreach (var altName in settings.AltNames) + { + if (string.IsNullOrWhiteSpace(altName)) + { + continue; + } + + var translatedAltName = Resources.ResourceManager.GetString(altName, CultureInfo.CurrentUICulture); + if (string.IsNullOrEmpty(translatedAltName)) + { + // Log.Warn($"Resource string for [{altName}] not found", typeof(TranslationHelper)); + } + + translatedAltNames.Add(translatedAltName ?? altName); + } + + settings.AltNames = translatedAltNames; + } + + // Translate Note + if (!string.IsNullOrWhiteSpace(settings.Note)) + { + var note = Resources.ResourceManager.GetString(settings.Note, CultureInfo.CurrentUICulture); + if (string.IsNullOrEmpty(note)) + { + // Log.Warn($"Resource string for [{settings.Note}] not found", typeof(TranslationHelper)); + } + + settings.Note = note ?? settings.Note ?? string.Empty; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs new file mode 100644 index 0000000000..b0e8a76ae9 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/UnsupportedSettingsHelper.cs @@ -0,0 +1,87 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers; + +/// +/// Helper class to easier work with the version of the Windows OS +/// +internal static class UnsupportedSettingsHelper +{ + private const string _keyPath = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + private const string _keyNameBuild = "CurrentBuild"; + private const string _keyNameBuildNumber = "CurrentBuildNumber"; + + /// + /// Remove all from the settings list in the given class. + /// + /// A class that contain all possible windows settings. + internal static void FilterByBuild(in Classes.WindowsSettings windowsSettings) + { + if (windowsSettings?.Settings is null) + { + return; + } + + var currentBuild = GetNumericRegistryValue(_keyPath, _keyNameBuild); + var currentBuildNumber = GetNumericRegistryValue(_keyPath, _keyNameBuildNumber); + + if (currentBuild != currentBuildNumber) + { + var usedValueName = currentBuild != uint.MinValue ? _keyNameBuild : _keyNameBuildNumber; + var warningMessage = + $"Detecting the Windows version in registry ({_keyPath}) leads to an inconclusive" + + $" result ({_keyNameBuild}={currentBuild}, {_keyNameBuildNumber}={currentBuildNumber})!" + + $" For resolving the conflict we use the value of '{usedValueName}'."; + + // TODO GH #108 Logging is something we have to take care of + // Log.Warn(warningMessage, typeof(UnsupportedSettingsHelper)); + } + + var currentWindowsBuild = currentBuild != uint.MinValue + ? currentBuild + : currentBuildNumber; + + var filteredSettingsList = windowsSettings.Settings.Where(found + => (found.DeprecatedInBuild == null || currentWindowsBuild < found.DeprecatedInBuild) + && (found.IntroducedInBuild == null || currentWindowsBuild >= found.IntroducedInBuild)); + + filteredSettingsList = filteredSettingsList.OrderBy(found => found.Name); + + windowsSettings.Settings = filteredSettingsList; + } + + /// + /// Return a unsigned numeric value from given registry value name inside the given registry key. + /// + /// The registry key. + /// The name of the registry value. + /// A registry value or on error. + private static uint GetNumericRegistryValue(in string registryKey, in string valueName) + { +#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + object? registryValueData; +#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + + try + { + registryValueData = Win32.Registry.GetValue(registryKey, valueName, uint.MinValue); + } + catch + { + // Log.Exception( + // $"Can't get registry value for '{valueName}'", + // exception, + // typeof(UnsupportedSettingsHelper)); + return uint.MinValue; + } + + return uint.TryParse(registryValueData as string, out var buildNumber) + ? buildNumber + : uint.MinValue; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/WindowsSettingsPathHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/WindowsSettingsPathHelper.cs new file mode 100644 index 0000000000..d7866d892f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/WindowsSettingsPathHelper.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers; + +/// +/// Helper class to help with the path of a . The settings path shows where to find a setting within Windows' user interface. +/// +internal static class WindowsSettingsPathHelper +{ + /// + /// The symbol which is used as delimiter between the parts of the path. + /// + private const string _pathDelimiterSequence = "\u0020\u0020\u02C3\u0020\u0020"; // = "" + + /// + /// Generates the values for and on all settings of the list in the given class. + /// + /// A class that contain all possible windows settings. + internal static void GenerateSettingsPathValues(in Classes.WindowsSettings windowsSettings) + { + if (windowsSettings?.Settings is null) + { + return; + } + + foreach (var settings in windowsSettings.Settings) + { + // Check if type value is filled. If not, then write log warning. + if (string.IsNullOrEmpty(settings.Type)) + { + // TODO GH #108 Logging is something we have to take care of + // Log.Warn($"The type property is not set for setting [{settings.Name}] in json. Skipping generating of settings path.", typeof(WindowsSettingsPathHelper)); + continue; + } + + // Check if "JoinedAreaPath" and "JoinedFullSettingsPath" are filled. Then log debug message. + if (!string.IsNullOrEmpty(settings.JoinedAreaPath)) + { + // Log.Debug($"The property [JoinedAreaPath] of setting [{settings.Name}] was filled from the json. This value is not used and will be overwritten.", typeof(WindowsSettingsPathHelper)); + } + + if (!string.IsNullOrEmpty(settings.JoinedFullSettingsPath)) + { + // TODO GH #108 Logging is something we have to take care of + // Log.Debug($"The property [JoinedFullSettingsPath] of setting [{settings.Name}] was filled from the json. This value is not used and will be overwritten.", typeof(WindowsSettingsPathHelper)); + } + + // Generating path values. + if (!(settings.Areas is null) && settings.Areas.Any()) + { + var areaValue = string.Join(_pathDelimiterSequence, settings.Areas); + settings.JoinedAreaPath = areaValue; + settings.JoinedFullSettingsPath = $"{settings.Type}{_pathDelimiterSequence}{areaValue}"; + } + else + { + settings.JoinedAreaPath = string.Empty; + settings.JoinedFullSettingsPath = settings.Type; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.dark.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.dark.png new file mode 100644 index 0000000000..888caf3249 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.dark.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.light.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.light.png new file mode 100644 index 0000000000..3bd8c9de21 Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Images/WindowsSettings.light.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj new file mode 100644 index 0000000000..d44e907606 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj @@ -0,0 +1,46 @@ + + + + Microsoft.CmdPal.Ext.WindowsSettings + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.WindowsSettings.pri + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + PreserveNewest + + + PreserveNewest + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs new file mode 100644 index 0000000000..994db10445 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Pages/WindowsSettingsListPage.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.WindowsSettings.Classes; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsSettings; + +internal sealed partial class WindowsSettingsListPage : DynamicListPage +{ + private readonly Classes.WindowsSettings _windowsSettings; + + public WindowsSettingsListPage(Classes.WindowsSettings windowsSettings) + { + Icon = IconHelpers.FromRelativePath("Assets\\WindowsSettings.svg"); + Name = "Windows Settings"; + Id = "com.microsoft.cmdpal.windowsSettings"; + _windowsSettings = windowsSettings; + } + + public List Query(string query) + { + if (_windowsSettings?.Settings is null) + { + return new List(0); + } + + var filteredList = _windowsSettings.Settings + .Where(Predicate) + .OrderBy(found => found.Name); + + var newList = ResultHelper.GetResultList(filteredList, query); + return newList; + + bool Predicate(WindowsSetting found) + { + if (string.IsNullOrWhiteSpace(query)) + { + // If no search string is entered skip query comparison. + return true; + } + + if (found.Name.Contains(query, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + + if (!(found.Areas is null)) + { + foreach (var area in found.Areas) + { + // Search for areas on normal queries. + if (area.Contains(query, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + + // Search for Area only on queries with action char. + if (area.Contains(query.Replace(":", string.Empty), StringComparison.CurrentCultureIgnoreCase) + && query.EndsWith(":", StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + } + } + + if (!(found.AltNames is null)) + { + foreach (var altName in found.AltNames) + { + if (altName.Contains(query, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + } + } + + // Search by key char '>' for app name and settings path + return query.Contains('>') ? ResultHelper.FilterBySettingsPath(found, query) : false; + } + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() + { + var items = Query(SearchText).ToArray(); + + return items; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..3a018b0e90 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.Designer.cs @@ -0,0 +1,4878 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.WindowsSettings.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.WindowsSettings.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to About. + /// + internal static string About { + get { + return ResourceManager.GetString("About", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to access.cpl. + /// + internal static string access_cpl { + get { + return ResourceManager.GetString("access.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accessibility Options. + /// + internal static string AccessibilityOptions { + get { + return ResourceManager.GetString("AccessibilityOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accessory apps. + /// + internal static string AccessoryApps { + get { + return ResourceManager.GetString("AccessoryApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Access work or school. + /// + internal static string AccessWorkOrSchool { + get { + return ResourceManager.GetString("AccessWorkOrSchool", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account info. + /// + internal static string AccountInfo { + get { + return ResourceManager.GetString("AccountInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accounts. + /// + internal static string Accounts { + get { + return ResourceManager.GetString("Accounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Action Center. + /// + internal static string ActionCenter { + get { + return ResourceManager.GetString("ActionCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Activation. + /// + internal static string Activation { + get { + return ResourceManager.GetString("Activation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Activity history. + /// + internal static string ActivityHistory { + get { + return ResourceManager.GetString("ActivityHistory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add devices. + /// + internal static string AddDevices { + get { + return ResourceManager.GetString("AddDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add Hardware. + /// + internal static string AddHardware { + get { + return ResourceManager.GetString("AddHardware", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add or remove programs. + /// + internal static string AddRemovePrograms { + get { + return ResourceManager.GetString("AddRemovePrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add your phone. + /// + internal static string AddYourPhone { + get { + return ResourceManager.GetString("AddYourPhone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Administrative Tools. + /// + internal static string AdministrativeTools { + get { + return ResourceManager.GetString("AdministrativeTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advanced display settings. + /// + internal static string AdvancedDisplaySettings { + get { + return ResourceManager.GetString("AdvancedDisplaySettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advanced graphics. + /// + internal static string AdvancedGraphics { + get { + return ResourceManager.GetString("AdvancedGraphics", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advertising ID. + /// + internal static string AdvertisingId { + get { + return ResourceManager.GetString("AdvertisingId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Airplane mode. + /// + internal static string AirplaneMode { + get { + return ResourceManager.GetString("AirplaneMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alt+Tab. + /// + internal static string AltAndTab { + get { + return ResourceManager.GetString("AltAndTab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alternative names. + /// + internal static string AlternativeName { + get { + return ResourceManager.GetString("AlternativeName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Animations. + /// + internal static string Animations { + get { + return ResourceManager.GetString("Animations", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to App color. + /// + internal static string AppColor { + get { + return ResourceManager.GetString("AppColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Control Panel. + /// + internal static string AppControlPanel { + get { + return ResourceManager.GetString("AppControlPanel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to App diagnostics. + /// + internal static string AppDiagnostics { + get { + return ResourceManager.GetString("AppDiagnostics", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to App features. + /// + internal static string AppFeatures { + get { + return ResourceManager.GetString("AppFeatures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to App. + /// + internal static string Application { + get { + return ResourceManager.GetString("Application", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Management Console. + /// + internal static string AppMMC { + get { + return ResourceManager.GetString("AppMMC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apps & Features. + /// + internal static string AppsAndFeatures { + get { + return ResourceManager.GetString("AppsAndFeatures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System settings. + /// + internal static string AppSettingsApp { + get { + return ResourceManager.GetString("AppSettingsApp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apps for websites. + /// + internal static string AppsForWebsites { + get { + return ResourceManager.GetString("AppsForWebsites", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to App volume and device preferences. + /// + internal static string AppVolumeAndDevicePreferences { + get { + return ResourceManager.GetString("AppVolumeAndDevicePreferences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to appwiz.cpl. + /// + internal static string appwiz_cpl { + get { + return ResourceManager.GetString("appwiz.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Area. + /// + internal static string Area { + get { + return ResourceManager.GetString("Area", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accounts. + /// + internal static string AreaAccounts { + get { + return ResourceManager.GetString("AreaAccounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Administrative Tools. + /// + internal static string AreaAdministrativeTools { + get { + return ResourceManager.GetString("AreaAdministrativeTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Appearance and Personalization. + /// + internal static string AreaAppearanceAndPersonalization { + get { + return ResourceManager.GetString("AreaAppearanceAndPersonalization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apps. + /// + internal static string AreaApps { + get { + return ResourceManager.GetString("AreaApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth & devices. + /// + internal static string AreaBluetoothAndDevices11 { + get { + return ResourceManager.GetString("AreaBluetoothAndDevices11", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clock and Region. + /// + internal static string AreaClockAndRegion { + get { + return ResourceManager.GetString("AreaClockAndRegion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cortana. + /// + internal static string AreaCortana { + get { + return ResourceManager.GetString("AreaCortana", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Devices. + /// + internal static string AreaDevices { + get { + return ResourceManager.GetString("AreaDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ease of access. + /// + internal static string AreaEaseOfAccess { + get { + return ResourceManager.GetString("AreaEaseOfAccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extras. + /// + internal static string AreaExtras { + get { + return ResourceManager.GetString("AreaExtras", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gaming. + /// + internal static string AreaGaming { + get { + return ResourceManager.GetString("AreaGaming", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hardware and Sound. + /// + internal static string AreaHardwareAndSound { + get { + return ResourceManager.GetString("AreaHardwareAndSound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mixed reality. + /// + internal static string AreaMixedReality { + get { + return ResourceManager.GetString("AreaMixedReality", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network and Internet. + /// + internal static string AreaNetworkAndInternet { + get { + return ResourceManager.GetString("AreaNetworkAndInternet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Personalization. + /// + internal static string AreaPersonalization { + get { + return ResourceManager.GetString("AreaPersonalization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone. + /// + internal static string AreaPhone { + get { + return ResourceManager.GetString("AreaPhone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Privacy. + /// + internal static string AreaPrivacy { + get { + return ResourceManager.GetString("AreaPrivacy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Programs. + /// + internal static string AreaPrograms { + get { + return ResourceManager.GetString("AreaPrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Security and Maintenance. + /// + internal static string AreaSecurityAndMaintenance { + get { + return ResourceManager.GetString("AreaSecurityAndMaintenance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SurfaceHub. + /// + internal static string AreaSurfaceHub { + get { + return ResourceManager.GetString("AreaSurfaceHub", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System. + /// + internal static string AreaSystem { + get { + return ResourceManager.GetString("AreaSystem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System and Security. + /// + internal static string AreaSystemAndSecurity { + get { + return ResourceManager.GetString("AreaSystemAndSecurity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System Properties. + /// + internal static string AreaSystemPropertiesAdvanced { + get { + return ResourceManager.GetString("AreaSystemPropertiesAdvanced", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time and language. + /// + internal static string AreaTimeAndLanguage { + get { + return ResourceManager.GetString("AreaTimeAndLanguage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update and security. + /// + internal static string AreaUpdateAndSecurity { + get { + return ResourceManager.GetString("AreaUpdateAndSecurity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User accounts. + /// + internal static string AreaUserAccounts { + get { + return ResourceManager.GetString("AreaUserAccounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assigned access. + /// + internal static string AssignedAccess { + get { + return ResourceManager.GetString("AssignedAccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Audio. + /// + internal static string Audio { + get { + return ResourceManager.GetString("Audio", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Audio alerts. + /// + internal static string AudioAlerts { + get { + return ResourceManager.GetString("AudioAlerts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Audio and speech. + /// + internal static string AudioAndSpeech { + get { + return ResourceManager.GetString("AudioAndSpeech", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authorization Manager. + /// + internal static string AuthorizationManager { + get { + return ResourceManager.GetString("AuthorizationManager", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatic file downloads. + /// + internal static string AutomaticFileDownloads { + get { + return ResourceManager.GetString("AutomaticFileDownloads", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AutoPlay. + /// + internal static string AutoPlay { + get { + return ResourceManager.GetString("AutoPlay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Background. + /// + internal static string Background { + get { + return ResourceManager.GetString("Background", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Background Apps. + /// + internal static string BackgroundApps { + get { + return ResourceManager.GetString("BackgroundApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Backup. + /// + internal static string Backup { + get { + return ResourceManager.GetString("Backup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Backup and Restore. + /// + internal static string BackupAndRestore { + get { + return ResourceManager.GetString("BackupAndRestore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery Saver. + /// + internal static string BatterySaver { + get { + return ResourceManager.GetString("BatterySaver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery Saver settings. + /// + internal static string BatterySaverSettings { + get { + return ResourceManager.GetString("BatterySaverSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery saver usage details. + /// + internal static string BatterySaverUsageDetails { + get { + return ResourceManager.GetString("BatterySaverUsageDetails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery use. + /// + internal static string BatteryUse { + get { + return ResourceManager.GetString("BatteryUse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Biometric Devices. + /// + internal static string BiometricDevices { + get { + return ResourceManager.GetString("BiometricDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BitLocker Drive Encryption. + /// + internal static string BitLockerDriveEncryption { + get { + return ResourceManager.GetString("BitLockerDriveEncryption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blue light. + /// + internal static string BlueLight { + get { + return ResourceManager.GetString("BlueLight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth. + /// + internal static string Bluetooth { + get { + return ResourceManager.GetString("Bluetooth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth and other devices. + /// + internal static string BluetoothAndDevices10 { + get { + return ResourceManager.GetString("BluetoothAndDevices10", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth devices. + /// + internal static string BluetoothDevices { + get { + return ResourceManager.GetString("BluetoothDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blue-yellow. + /// + internal static string BlueYellow { + get { + return ResourceManager.GetString("BlueYellow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bopomofo IME. + /// + internal static string BopomofoIme { + get { + return ResourceManager.GetString("BopomofoIme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to bpmf. + /// + internal static string bpmf { + get { + return ResourceManager.GetString("bpmf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Broadcasting. + /// + internal static string Broadcasting { + get { + return ResourceManager.GetString("Broadcasting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to bthprops.cpl. + /// + internal static string bthprops_cpl { + get { + return ResourceManager.GetString("bthprops.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calendar. + /// + internal static string Calendar { + get { + return ResourceManager.GetString("Calendar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Call history. + /// + internal static string CallHistory { + get { + return ResourceManager.GetString("CallHistory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to calling. + /// + internal static string calling { + get { + return ResourceManager.GetString("calling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Camera. + /// + internal static string Camera { + get { + return ResourceManager.GetString("Camera", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cangjie IME. + /// + internal static string CangjieIme { + get { + return ResourceManager.GetString("CangjieIme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Caps Lock. + /// + internal static string CapsLock { + get { + return ResourceManager.GetString("CapsLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cellular and SIM. + /// + internal static string CellularAndSim { + get { + return ResourceManager.GetString("CellularAndSim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Certificates - Current User. + /// + internal static string CertificatesCurrentUser { + get { + return ResourceManager.GetString("CertificatesCurrentUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Certificates - Local Computer. + /// + internal static string CertificatesLocalComputer { + get { + return ResourceManager.GetString("CertificatesLocalComputer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change programs. + /// + internal static string ChangePrograms { + get { + return ResourceManager.GetString("ChangePrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change screen saver. + /// + internal static string ChangeScreenSaver { + get { + return ResourceManager.GetString("ChangeScreenSaver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change User Account Control settings. + /// + internal static string ChangeUACSettings { + get { + return ResourceManager.GetString("ChangeUACSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose which folders appear on Start. + /// + internal static string ChooseWhichFoldersAppearOnStart { + get { + return ResourceManager.GetString("ChooseWhichFoldersAppearOnStart", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Client service for NetWare. + /// + internal static string ClientServiceForNetWare { + get { + return ResourceManager.GetString("ClientServiceForNetWare", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clipboard. + /// + internal static string Clipboard { + get { + return ResourceManager.GetString("Clipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closed captions. + /// + internal static string ClosedCaptions { + get { + return ResourceManager.GetString("ClosedCaptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to collab.cpl. + /// + internal static string collab_cpl { + get { + return ResourceManager.GetString("collab.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color filters. + /// + internal static string ColorFilters { + get { + return ResourceManager.GetString("ColorFilters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color management. + /// + internal static string ColorManagement { + get { + return ResourceManager.GetString("ColorManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Colors. + /// + internal static string Colors { + get { + return ResourceManager.GetString("Colors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command. + /// + internal static string Command { + get { + return ResourceManager.GetString("Command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to COM-Objects. + /// + internal static string ComObjects { + get { + return ResourceManager.GetString("ComObjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Component Services. + /// + internal static string ComponentServices { + get { + return ResourceManager.GetString("ComponentServices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Computer Management. + /// + internal static string ComputerManagement { + get { + return ResourceManager.GetString("ComputerManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connectable devices. + /// + internal static string ConnectableDevices { + get { + return ResourceManager.GetString("ConnectableDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connected Devices. + /// + internal static string ConnectedDevices { + get { + return ResourceManager.GetString("ConnectedDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect panel. + /// + internal static string ConnectPanel { + get { + return ResourceManager.GetString("ConnectPanel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to a wireless audio device. + /// + internal static string ConnectWirelessAudio { + get { + return ResourceManager.GetString("ConnectWirelessAudio", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to a wireless display. + /// + internal static string ConnectWirelessDisplay { + get { + return ResourceManager.GetString("ConnectWirelessDisplay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Contacts. + /// + internal static string Contacts { + get { + return ResourceManager.GetString("Contacts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy command. + /// + internal static string CopyCommand { + get { + return ResourceManager.GetString("CopyCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Core Isolation. + /// + internal static string CoreIsolation { + get { + return ResourceManager.GetString("CoreIsolation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cortana. + /// + internal static string Cortana { + get { + return ResourceManager.GetString("Cortana", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cortana across my devices. + /// + internal static string CortanaAcrossMyDevices { + get { + return ResourceManager.GetString("CortanaAcrossMyDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cortana - Language. + /// + internal static string CortanaLanguage { + get { + return ResourceManager.GetString("CortanaLanguage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create and format hard disk partitions. + /// + internal static string CreateAndFormatHardDiskPartitions { + get { + return ResourceManager.GetString("CreateAndFormatHardDiskPartitions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Credential manager. + /// + internal static string CredentialManager { + get { + return ResourceManager.GetString("CredentialManager", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crossdevice. + /// + internal static string Crossdevice { + get { + return ResourceManager.GetString("Crossdevice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Custom devices. + /// + internal static string CustomDevices { + get { + return ResourceManager.GetString("CustomDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dark color. + /// + internal static string DarkColor { + get { + return ResourceManager.GetString("DarkColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dark mode. + /// + internal static string DarkMode { + get { + return ResourceManager.GetString("DarkMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data usage. + /// + internal static string DataUsage { + get { + return ResourceManager.GetString("DataUsage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time. + /// + internal static string DateAndTime { + get { + return ResourceManager.GetString("DateAndTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default apps. + /// + internal static string DefaultApps { + get { + return ResourceManager.GetString("DefaultApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default camera. + /// + internal static string DefaultCamera { + get { + return ResourceManager.GetString("DefaultCamera", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default location. + /// + internal static string DefaultLocation { + get { + return ResourceManager.GetString("DefaultLocation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default programs. + /// + internal static string DefaultPrograms { + get { + return ResourceManager.GetString("DefaultPrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default Save Locations. + /// + internal static string DefaultSaveLocations { + get { + return ResourceManager.GetString("DefaultSaveLocations", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Defender Firewall with Advanced Security. + /// + internal static string DefenderFirewallAdvancedSecurity { + get { + return ResourceManager.GetString("DefenderFirewallAdvancedSecurity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delivery Optimization. + /// + internal static string DeliveryOptimization { + get { + return ResourceManager.GetString("DeliveryOptimization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to desk.cpl. + /// + internal static string desk_cpl { + get { + return ResourceManager.GetString("desk.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Desktop themes. + /// + internal static string DesktopThemes { + get { + return ResourceManager.GetString("DesktopThemes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to deuteranopia. + /// + internal static string deuteranopia { + get { + return ResourceManager.GetString("deuteranopia", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Device discovery. + /// + internal static string DeviceDiscovery { + get { + return ResourceManager.GetString("DeviceDiscovery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Device manager. + /// + internal static string DeviceManager { + get { + return ResourceManager.GetString("DeviceManager", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Device Manager. + /// + internal static string DeviceManagerSnapIn { + get { + return ResourceManager.GetString("DeviceManagerSnapIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Devices. + /// + internal static string Devices { + get { + return ResourceManager.GetString("Devices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Devices and printers. + /// + internal static string DevicesAndPrinters { + get { + return ResourceManager.GetString("DevicesAndPrinters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DHCP. + /// + internal static string Dhcp { + get { + return ResourceManager.GetString("Dhcp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dial-up. + /// + internal static string DialUp { + get { + return ResourceManager.GetString("DialUp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Direct access. + /// + internal static string DirectAccess { + get { + return ResourceManager.GetString("DirectAccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Direct open your phone. + /// + internal static string DirectOpenYourPhone { + get { + return ResourceManager.GetString("DirectOpenYourPhone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disk Management. + /// + internal static string DiskManagement { + get { + return ResourceManager.GetString("DiskManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display. + /// + internal static string Display { + get { + return ResourceManager.GetString("Display", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display properties. + /// + internal static string DisplayProperties { + get { + return ResourceManager.GetString("DisplayProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DNS. + /// + internal static string DNS { + get { + return ResourceManager.GetString("DNS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Documents. + /// + internal static string Documents { + get { + return ResourceManager.GetString("Documents", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicating my display. + /// + internal static string DuplicatingMyDisplay { + get { + return ResourceManager.GetString("DuplicatingMyDisplay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to During these hours. + /// + internal static string DuringTheseHours { + get { + return ResourceManager.GetString("DuringTheseHours", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ease of access center. + /// + internal static string EaseOfAccessCenter { + get { + return ResourceManager.GetString("EaseOfAccessCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit environment variables. + /// + internal static string EditEnvironmentVariables { + get { + return ResourceManager.GetString("EditEnvironmentVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edition. + /// + internal static string Edition { + get { + return ResourceManager.GetString("Edition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit the system environment variables. + /// + internal static string EditSystemEnvironmentVariables { + get { + return ResourceManager.GetString("EditSystemEnvironmentVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit environment variables for your account. + /// + internal static string EditUserEnvironmentVariables { + get { + return ResourceManager.GetString("EditUserEnvironmentVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email. + /// + internal static string Email { + get { + return ResourceManager.GetString("Email", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email and app accounts. + /// + internal static string EmailAndAppAccounts { + get { + return ResourceManager.GetString("EmailAndAppAccounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encryption. + /// + internal static string Encryption { + get { + return ResourceManager.GetString("Encryption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Environment. + /// + internal static string Environment { + get { + return ResourceManager.GetString("Environment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Env vars. + /// + internal static string EnvVars { + get { + return ResourceManager.GetString("EnvVars", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ethernet. + /// + internal static string Ethernet { + get { + return ResourceManager.GetString("Ethernet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event Viewer. + /// + internal static string EventViewer { + get { + return ResourceManager.GetString("EventViewer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exploit Protection. + /// + internal static string ExploitProtection { + get { + return ResourceManager.GetString("ExploitProtection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extras. + /// + internal static string Extras { + get { + return ResourceManager.GetString("Extras", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Eye control. + /// + internal static string EyeControl { + get { + return ResourceManager.GetString("EyeControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Eye tracker. + /// + internal static string EyeTracker { + get { + return ResourceManager.GetString("EyeTracker", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Family and other people. + /// + internal static string FamilyAndOtherPeople { + get { + return ResourceManager.GetString("FamilyAndOtherPeople", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Feedback and diagnostics. + /// + internal static string FeedbackAndDiagnostics { + get { + return ResourceManager.GetString("FeedbackAndDiagnostics", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File system. + /// + internal static string FileSystem { + get { + return ResourceManager.GetString("FileSystem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to FindFast. + /// + internal static string FindFast { + get { + return ResourceManager.GetString("FindFast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to findfast.cpl. + /// + internal static string findfast_cpl { + get { + return ResourceManager.GetString("findfast.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Find My Device. + /// + internal static string FindMyDevice { + get { + return ResourceManager.GetString("FindMyDevice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Firewall. + /// + internal static string Firewall { + get { + return ResourceManager.GetString("Firewall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Firewall.cpl. + /// + internal static string Firewall_cpl { + get { + return ResourceManager.GetString("Firewall.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Focus assist - Quiet hours. + /// + internal static string FocusAssistQuietHours { + get { + return ResourceManager.GetString("FocusAssistQuietHours", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Focus assist - Quiet moments. + /// + internal static string FocusAssistQuietMoments { + get { + return ResourceManager.GetString("FocusAssistQuietMoments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Folder options. + /// + internal static string FolderOptions { + get { + return ResourceManager.GetString("FolderOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fonts. + /// + internal static string Fonts { + get { + return ResourceManager.GetString("Fonts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For developers. + /// + internal static string ForDevelopers { + get { + return ResourceManager.GetString("ForDevelopers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game bar. + /// + internal static string GameBar { + get { + return ResourceManager.GetString("GameBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game controllers. + /// + internal static string GameControllers { + get { + return ResourceManager.GetString("GameControllers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game DVR. + /// + internal static string GameDvr { + get { + return ResourceManager.GetString("GameDvr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game Mode. + /// + internal static string GameMode { + get { + return ResourceManager.GetString("GameMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gateway. + /// + internal static string Gateway { + get { + return ResourceManager.GetString("Gateway", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to General. + /// + internal static string General { + get { + return ResourceManager.GetString("General", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get programs. + /// + internal static string GetPrograms { + get { + return ResourceManager.GetString("GetPrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Getting started. + /// + internal static string GettingStarted { + get { + return ResourceManager.GetString("GettingStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Glance. + /// + internal static string Glance { + get { + return ResourceManager.GetString("Glance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GPT. + /// + internal static string GPT { + get { + return ResourceManager.GetString("GPT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Graphics settings. + /// + internal static string GraphicsSettings { + get { + return ResourceManager.GetString("GraphicsSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Grayscale. + /// + internal static string Grayscale { + get { + return ResourceManager.GetString("Grayscale", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Green week. + /// + internal static string GreenWeek { + get { + return ResourceManager.GetString("GreenWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Group Policy. + /// + internal static string GroupPolicy { + get { + return ResourceManager.GetString("GroupPolicy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to hdwwiz.cpl. + /// + internal static string hdwwiz_cpl { + get { + return ResourceManager.GetString("hdwwiz.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Headset display. + /// + internal static string HeadsetDisplay { + get { + return ResourceManager.GetString("HeadsetDisplay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to High contrast. + /// + internal static string HighContrast { + get { + return ResourceManager.GetString("HighContrast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Holographic audio. + /// + internal static string HolographicAudio { + get { + return ResourceManager.GetString("HolographicAudio", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Holographic Environment. + /// + internal static string HolographicEnvironment { + get { + return ResourceManager.GetString("HolographicEnvironment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Holographic Headset. + /// + internal static string HolographicHeadset { + get { + return ResourceManager.GetString("HolographicHeadset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Holographic Management. + /// + internal static string HolographicManagement { + get { + return ResourceManager.GetString("HolographicManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Home group. + /// + internal static string HomeGroup { + get { + return ResourceManager.GetString("HomeGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ID. + /// + internal static string Id { + get { + return ResourceManager.GetString("Id", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image. + /// + internal static string Image { + get { + return ResourceManager.GetString("Image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indexing options. + /// + internal static string IndexingOptions { + get { + return ResourceManager.GetString("IndexingOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to inetcpl.cpl. + /// + internal static string inetcpl_cpl { + get { + return ResourceManager.GetString("inetcpl.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Infrared. + /// + internal static string Infrared { + get { + return ResourceManager.GetString("Infrared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inking and typing. + /// + internal static string InkingAndTyping { + get { + return ResourceManager.GetString("InkingAndTyping", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Internet options. + /// + internal static string InternetOptions { + get { + return ResourceManager.GetString("InternetOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to intl.cpl. + /// + internal static string intl_cpl { + get { + return ResourceManager.GetString("intl.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inverted colors. + /// + internal static string InvertedColors { + get { + return ResourceManager.GetString("InvertedColors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IP. + /// + internal static string Ip { + get { + return ResourceManager.GetString("Ip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IP Security Monitor. + /// + internal static string IpSecurityMonitor { + get { + return ResourceManager.GetString("IpSecurityMonitor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IP Security Policies on Local Computer. + /// + internal static string IpSecurityPoliciesOnLocalComputer { + get { + return ResourceManager.GetString("IpSecurityPoliciesOnLocalComputer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to irprops.cpl. + /// + internal static string irprops_cpl { + get { + return ResourceManager.GetString("irprops.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Isolated Browsing. + /// + internal static string IsolatedBrowsing { + get { + return ResourceManager.GetString("IsolatedBrowsing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Japan IME settings. + /// + internal static string JapanImeSettings { + get { + return ResourceManager.GetString("JapanImeSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to joy.cpl. + /// + internal static string joy_cpl { + get { + return ResourceManager.GetString("joy.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Joystick properties. + /// + internal static string JoystickProperties { + get { + return ResourceManager.GetString("JoystickProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to jpnime. + /// + internal static string jpnime { + get { + return ResourceManager.GetString("jpnime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keyboard. + /// + internal static string Keyboard { + get { + return ResourceManager.GetString("Keyboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keypad. + /// + internal static string Keypad { + get { + return ResourceManager.GetString("Keypad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keys. + /// + internal static string Keys { + get { + return ResourceManager.GetString("Keys", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + internal static string Language { + get { + return ResourceManager.GetString("Language", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Light color. + /// + internal static string LightColor { + get { + return ResourceManager.GetString("LightColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Light mode. + /// + internal static string LightMode { + get { + return ResourceManager.GetString("LightMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local Computer Policy. + /// + internal static string LocalGroupPolicy { + get { + return ResourceManager.GetString("LocalGroupPolicy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local Users and Groups. + /// + internal static string LocalUsersAndGroups { + get { + return ResourceManager.GetString("LocalUsersAndGroups", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location. + /// + internal static string Location { + get { + return ResourceManager.GetString("Location", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock screen. + /// + internal static string LockScreen { + get { + return ResourceManager.GetString("LockScreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Magnifier. + /// + internal static string Magnifier { + get { + return ResourceManager.GetString("Magnifier", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mail - Microsoft Exchange or Windows Messaging. + /// + internal static string MailMicrosoftExchangeOrWindowsMessaging { + get { + return ResourceManager.GetString("MailMicrosoftExchangeOrWindowsMessaging", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to main.cpl. + /// + internal static string main_cpl { + get { + return ResourceManager.GetString("main.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manage devices. + /// + internal static string ManageDevices { + get { + return ResourceManager.GetString("ManageDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manage known networks. + /// + internal static string ManageKnownNetworks { + get { + return ResourceManager.GetString("ManageKnownNetworks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manage optional features. + /// + internal static string ManageOptionalFeatures { + get { + return ResourceManager.GetString("ManageOptionalFeatures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MBR. + /// + internal static string MBR { + get { + return ResourceManager.GetString("MBR", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Messaging. + /// + internal static string Messaging { + get { + return ResourceManager.GetString("Messaging", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metered connection. + /// + internal static string MeteredConnection { + get { + return ResourceManager.GetString("MeteredConnection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microphone. + /// + internal static string Microphone { + get { + return ResourceManager.GetString("Microphone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Mail Post Office. + /// + internal static string MicrosoftMailPostOffice { + get { + return ResourceManager.GetString("MicrosoftMailPostOffice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to mlcfg32.cpl. + /// + internal static string mlcfg32_cpl { + get { + return ResourceManager.GetString("mlcfg32.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to azman.msc. + /// + internal static string MMC_azman { + get { + return ResourceManager.GetString("MMC_azman", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to certlm.msc. + /// + internal static string MMC_certlm { + get { + return ResourceManager.GetString("MMC_certlm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to certmgr.msc. + /// + internal static string MMC_certmgr { + get { + return ResourceManager.GetString("MMC_certmgr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to comexp.msc. + /// + internal static string MMC_comexp { + get { + return ResourceManager.GetString("MMC_comexp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to compmgmt.msc. + /// + internal static string MMC_compmgmt { + get { + return ResourceManager.GetString("MMC_compmgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to devmgmt.msc. + /// + internal static string MMC_devmgmt { + get { + return ResourceManager.GetString("MMC_devmgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to diskmgmt.msc. + /// + internal static string MMC_diskmgmt { + get { + return ResourceManager.GetString("MMC_diskmgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to eventvwr.msc. + /// + internal static string MMC_eventvwr { + get { + return ResourceManager.GetString("MMC_eventvwr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to fsmgmt.msc. + /// + internal static string MMC_fsmgmt { + get { + return ResourceManager.GetString("MMC_fsmgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to gpedit.msc. + /// + internal static string MMC_gpedit { + get { + return ResourceManager.GetString("MMC_gpedit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to lusrmgr.msc. + /// + internal static string MMC_lusrmgr { + get { + return ResourceManager.GetString("MMC_lusrmgr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to mmc.exe. + /// + internal static string MMC_mmcexe { + get { + return ResourceManager.GetString("MMC_mmcexe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to perfmon.msc. + /// + internal static string MMC_perfmon { + get { + return ResourceManager.GetString("MMC_perfmon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to printmanagement.msc. + /// + internal static string MMC_printmanagement { + get { + return ResourceManager.GetString("MMC_printmanagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to rsop.msc. + /// + internal static string MMC_rsop { + get { + return ResourceManager.GetString("MMC_rsop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to secpol.msc. + /// + internal static string MMC_secpol { + get { + return ResourceManager.GetString("MMC_secpol", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to services.msc. + /// + internal static string MMC_services { + get { + return ResourceManager.GetString("MMC_services", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to taskschd.msc. + /// + internal static string MMC_taskschd { + get { + return ResourceManager.GetString("MMC_taskschd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to tpm.msc. + /// + internal static string MMC_tpm { + get { + return ResourceManager.GetString("MMC_tpm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WF.msc. + /// + internal static string MMC_wf { + get { + return ResourceManager.GetString("MMC_wf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WmiMgmt.msc. + /// + internal static string MMC_wmimgmt { + get { + return ResourceManager.GetString("MMC_wmimgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to mmsys.cpl. + /// + internal static string mmsys_cpl { + get { + return ResourceManager.GetString("mmsys.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mobile devices. + /// + internal static string MobileDevices { + get { + return ResourceManager.GetString("MobileDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mobile hotspot. + /// + internal static string MobileHotspot { + get { + return ResourceManager.GetString("MobileHotspot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to modem.cpl. + /// + internal static string modem_cpl { + get { + return ResourceManager.GetString("modem.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mono. + /// + internal static string Mono { + get { + return ResourceManager.GetString("Mono", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to More details. + /// + internal static string MoreDetails { + get { + return ResourceManager.GetString("MoreDetails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Motion. + /// + internal static string Motion { + get { + return ResourceManager.GetString("Motion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mouse. + /// + internal static string Mouse { + get { + return ResourceManager.GetString("Mouse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mouse and touchpad. + /// + internal static string MouseAndTouchpad { + get { + return ResourceManager.GetString("MouseAndTouchpad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mouse, Fonts, Keyboard, and Printers properties. + /// + internal static string MouseFontsKeyboardAndPrintersProperties { + get { + return ResourceManager.GetString("MouseFontsKeyboardAndPrintersProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mouse pointer. + /// + internal static string MousePointer { + get { + return ResourceManager.GetString("MousePointer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multimedia properties. + /// + internal static string MultimediaProperties { + get { + return ResourceManager.GetString("MultimediaProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multitasking. + /// + internal static string Multitasking { + get { + return ResourceManager.GetString("Multitasking", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Narrator. + /// + internal static string Narrator { + get { + return ResourceManager.GetString("Narrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navigation bar. + /// + internal static string NavigationBar { + get { + return ResourceManager.GetString("NavigationBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ncpa.cpl. + /// + internal static string ncpa_cpl { + get { + return ResourceManager.GetString("ncpa.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nearby sharing settings. + /// + internal static string NearbyShareSettings { + get { + return ResourceManager.GetString("NearbyShareSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to netcpl.cpl. + /// + internal static string netcpl_cpl { + get { + return ResourceManager.GetString("netcpl.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to netsetup.cpl. + /// + internal static string netsetup_cpl { + get { + return ResourceManager.GetString("netsetup.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network. + /// + internal static string Network { + get { + return ResourceManager.GetString("Network", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network and sharing center. + /// + internal static string NetworkAndSharingCenter { + get { + return ResourceManager.GetString("NetworkAndSharingCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network connection. + /// + internal static string NetworkConnection { + get { + return ResourceManager.GetString("NetworkConnection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network properties. + /// + internal static string NetworkProperties { + get { + return ResourceManager.GetString("NetworkProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network sessions. + /// + internal static string NetworkSessions { + get { + return ResourceManager.GetString("NetworkSessions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network Setup Wizard. + /// + internal static string NetworkSetupWizard { + get { + return ResourceManager.GetString("NetworkSetupWizard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Network status. + /// + internal static string NetworkStatus { + get { + return ResourceManager.GetString("NetworkStatus", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NFC. + /// + internal static string NFC { + get { + return ResourceManager.GetString("NFC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NFC Transactions. + /// + internal static string NFCTransactions { + get { + return ResourceManager.GetString("NFCTransactions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Night light. + /// + internal static string NightLight { + get { + return ResourceManager.GetString("NightLight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Night light settings. + /// + internal static string NightLightSettings { + get { + return ResourceManager.GetString("NightLightSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Note. + /// + internal static string Note { + get { + return ResourceManager.GetString("Note", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available when you have connected a mobile device to your device.. + /// + internal static string NoteAddYourPhone { + get { + return ResourceManager.GetString("NoteAddYourPhone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available on devices that support advanced graphics options.. + /// + internal static string NoteAdvancedGraphics { + get { + return ResourceManager.GetString("NoteAdvancedGraphics", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available on devices that have a battery, such as a tablet.. + /// + internal static string NoteBattery { + get { + return ResourceManager.GetString("NoteBattery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deprecated in Windows 10, version 1809 (build 17763) and later.. + /// + internal static string NoteDeprecated17763 { + get { + return ResourceManager.GetString("NoteDeprecated17763", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if Dial is paired.. + /// + internal static string NoteDialPaired { + get { + return ResourceManager.GetString("NoteDialPaired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if DirectAccess is enabled.. + /// + internal static string NoteDirectAccess { + get { + return ResourceManager.GetString("NoteDirectAccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available on devices that support advanced display options.. + /// + internal static string NoteDisplayGraphics { + get { + return ResourceManager.GetString("NoteDisplayGraphics", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Editing this setting may require administrative privileges.. + /// + internal static string NoteEditingRequireAdminPrivileges { + get { + return ResourceManager.GetString("NoteEditingRequireAdminPrivileges", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only present if user is enrolled in WIP.. + /// + internal static string NoteEnrolledWIP { + get { + return ResourceManager.GetString("NoteEnrolledWIP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requires eyetracker hardware.. + /// + internal static string NoteEyetrackerHardware { + get { + return ResourceManager.GetString("NoteEyetrackerHardware", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available if the Microsoft Japan input method editor is installed.. + /// + internal static string NoteImeJapan { + get { + return ResourceManager.GetString("NoteImeJapan", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available if the Microsoft Pinyin input method editor is installed.. + /// + internal static string NoteImePinyin { + get { + return ResourceManager.GetString("NoteImePinyin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available if the Microsoft Wubi input method editor is installed.. + /// + internal static string NoteImeWubi { + get { + return ResourceManager.GetString("NoteImeWubi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if the Mixed Reality Portal app is installed.. + /// + internal static string NoteMixedReality { + get { + return ResourceManager.GetString("NoteMixedReality", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available on mobile and if the enterprise has deployed a provisioning package.. + /// + internal static string NoteMobileProvisioning { + get { + return ResourceManager.GetString("NoteMobileProvisioning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have to add this snap-in manually.. + /// + internal static string NoteNoMscFileExist { + get { + return ResourceManager.GetString("NoteNoMscFileExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Added in Windows 10, version 1903 (build 18362).. + /// + internal static string NoteSince18362 { + get { + return ResourceManager.GetString("NoteSince18362", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Added in Windows 10, version 2004 (build 19041).. + /// + internal static string NoteSince19041 { + get { + return ResourceManager.GetString("NoteSince19041", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if "settings apps" are installed, for example, by a 3rd party.. + /// + internal static string NoteThirdParty { + get { + return ResourceManager.GetString("NoteThirdParty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if touchpad hardware is present.. + /// + internal static string NoteTouchpad { + get { + return ResourceManager.GetString("NoteTouchpad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if the device has a Wi-Fi adapter.. + /// + internal static string NoteWiFiAdapter { + get { + return ResourceManager.GetString("NoteWiFiAdapter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Device must be Windows Anywhere-capable.. + /// + internal static string NoteWindowsAnywhere { + get { + return ResourceManager.GetString("NoteWindowsAnywhere", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only available if enterprise has deployed a provisioning package.. + /// + internal static string NoteWorkplaceProvisioning { + get { + return ResourceManager.GetString("NoteWorkplaceProvisioning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Notifications. + /// + internal static string Notifications { + get { + return ResourceManager.GetString("Notifications", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Notifications and actions. + /// + internal static string NotificationsAndActions { + get { + return ResourceManager.GetString("NotificationsAndActions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Num Lock. + /// + internal static string NumLock { + get { + return ResourceManager.GetString("NumLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to nwc.cpl. + /// + internal static string nwc_cpl { + get { + return ResourceManager.GetString("nwc.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to odbccp32.cpl. + /// + internal static string odbccp32_cpl { + get { + return ResourceManager.GetString("odbccp32.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ODBC Data Source Administrator (32-bit). + /// + internal static string OdbcDataSourceAdministrator32Bit { + get { + return ResourceManager.GetString("OdbcDataSourceAdministrator32Bit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ODBC Data Source Administrator (64-bit). + /// + internal static string OdbcDataSourceAdministrator64Bit { + get { + return ResourceManager.GetString("OdbcDataSourceAdministrator64Bit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offline files. + /// + internal static string OfflineFiles { + get { + return ResourceManager.GetString("OfflineFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offline Maps. + /// + internal static string OfflineMaps { + get { + return ResourceManager.GetString("OfflineMaps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offline Maps - Download maps. + /// + internal static string OfflineMapsDownloadMaps { + get { + return ResourceManager.GetString("OfflineMapsDownloadMaps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On-Screen. + /// + internal static string OnScreen { + get { + return ResourceManager.GetString("OnScreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Control Panel (Application homepage). + /// + internal static string OpenControlPanel { + get { + return ResourceManager.GetString("OpenControlPanel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Settings. + /// + internal static string OpenSettings { + get { + return ResourceManager.GetString("OpenSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings (Application homepage). + /// + internal static string OpenSettingsApp { + get { + return ResourceManager.GetString("OpenSettingsApp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OS. + /// + internal static string Os { + get { + return ResourceManager.GetString("Os", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Other devices. + /// + internal static string OtherDevices { + get { + return ResourceManager.GetString("OtherDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Other options. + /// + internal static string OtherOptions { + get { + return ResourceManager.GetString("OtherOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Other users. + /// + internal static string OtherUsers { + get { + return ResourceManager.GetString("OtherUsers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parental controls. + /// + internal static string ParentalControls { + get { + return ResourceManager.GetString("ParentalControls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + internal static string Password { + get { + return ResourceManager.GetString("Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to password.cpl. + /// + internal static string password_cpl { + get { + return ResourceManager.GetString("password.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password properties. + /// + internal static string PasswordProperties { + get { + return ResourceManager.GetString("PasswordProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pen and input devices. + /// + internal static string PenAndInputDevices { + get { + return ResourceManager.GetString("PenAndInputDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pen and touch. + /// + internal static string PenAndTouch { + get { + return ResourceManager.GetString("PenAndTouch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pen and Windows Ink. + /// + internal static string PenAndWindowsInk { + get { + return ResourceManager.GetString("PenAndWindowsInk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to People Near Me. + /// + internal static string PeopleNearMe { + get { + return ResourceManager.GetString("PeopleNearMe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Performance information and tools. + /// + internal static string PerformanceInformationAndTools { + get { + return ResourceManager.GetString("PerformanceInformationAndTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Performance Monitor. + /// + internal static string PerformanceMonitor { + get { + return ResourceManager.GetString("PerformanceMonitor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Permissions and history. + /// + internal static string PermissionsAndHistory { + get { + return ResourceManager.GetString("PermissionsAndHistory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Personalization (category). + /// + internal static string PersonalizationCategory { + get { + return ResourceManager.GetString("PersonalizationCategory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone. + /// + internal static string Phone { + get { + return ResourceManager.GetString("Phone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone and modem. + /// + internal static string PhoneAndModem { + get { + return ResourceManager.GetString("PhoneAndModem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone and modem - Options. + /// + internal static string PhoneAndModemOptions { + get { + return ResourceManager.GetString("PhoneAndModemOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone calls. + /// + internal static string PhoneCalls { + get { + return ResourceManager.GetString("PhoneCalls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone - Default apps. + /// + internal static string PhoneDefaultApps { + get { + return ResourceManager.GetString("PhoneDefaultApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone Link. + /// + internal static string PhoneLink { + get { + return ResourceManager.GetString("PhoneLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Picture. + /// + internal static string Picture { + get { + return ResourceManager.GetString("Picture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pictures. + /// + internal static string Pictures { + get { + return ResourceManager.GetString("Pictures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pinyin IME settings. + /// + internal static string PinyinImeSettings { + get { + return ResourceManager.GetString("PinyinImeSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pinyin IME settings - domain lexicon. + /// + internal static string PinyinImeSettingsDomainLexicon { + get { + return ResourceManager.GetString("PinyinImeSettingsDomainLexicon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pinyin IME settings - Key configuration. + /// + internal static string PinyinImeSettingsKeyConfiguration { + get { + return ResourceManager.GetString("PinyinImeSettingsKeyConfiguration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pinyin IME settings - UDP. + /// + internal static string PinyinImeSettingsUdp { + get { + return ResourceManager.GetString("PinyinImeSettingsUdp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playing a game full screen. + /// + internal static string PlayingGameFullScreen { + get { + return ResourceManager.GetString("PlayingGameFullScreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searches Windows settings. + /// + internal static string PluginDescription { + get { + return ResourceManager.GetString("PluginDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows settings. + /// + internal static string PluginTitle { + get { + return ResourceManager.GetString("PluginTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PNP Device. + /// + internal static string PnpDevice { + get { + return ResourceManager.GetString("PnpDevice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Power and sleep. + /// + internal static string PowerAndSleep { + get { + return ResourceManager.GetString("PowerAndSleep", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to powercfg.cpl. + /// + internal static string powercfg_cpl { + get { + return ResourceManager.GetString("powercfg.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Power options. + /// + internal static string PowerOptions { + get { + return ResourceManager.GetString("PowerOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Presentation. + /// + internal static string Presentation { + get { + return ResourceManager.GetString("Presentation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Printers. + /// + internal static string Printers { + get { + return ResourceManager.GetString("Printers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Printers and scanners. + /// + internal static string PrintersAndScanners { + get { + return ResourceManager.GetString("PrintersAndScanners", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Printer Spooler. + /// + internal static string PrinterSpooler { + get { + return ResourceManager.GetString("PrinterSpooler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Print Management. + /// + internal static string PrintManagement { + get { + return ResourceManager.GetString("PrintManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Print screen. + /// + internal static string PrintScreen { + get { + return ResourceManager.GetString("PrintScreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Problem reports and solutions. + /// + internal static string ProblemReportsAndSolutions { + get { + return ResourceManager.GetString("ProblemReportsAndSolutions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processor. + /// + internal static string Processor { + get { + return ResourceManager.GetString("Processor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Programs and features. + /// + internal static string ProgramsAndFeatures { + get { + return ResourceManager.GetString("ProgramsAndFeatures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Projecting to this PC. + /// + internal static string ProjectingToThisPc { + get { + return ResourceManager.GetString("ProjectingToThisPc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to protanopia. + /// + internal static string protanopia { + get { + return ResourceManager.GetString("protanopia", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provisioning. + /// + internal static string Provisioning { + get { + return ResourceManager.GetString("Provisioning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proximity. + /// + internal static string Proximity { + get { + return ResourceManager.GetString("Proximity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy. + /// + internal static string Proxy { + get { + return ResourceManager.GetString("Proxy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quickime. + /// + internal static string Quickime { + get { + return ResourceManager.GetString("Quickime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quiet moments game. + /// + internal static string QuietMomentsGame { + get { + return ResourceManager.GetString("QuietMomentsGame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Radios. + /// + internal static string Radios { + get { + return ResourceManager.GetString("Radios", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RAM. + /// + internal static string Ram { + get { + return ResourceManager.GetString("Ram", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recognition. + /// + internal static string Recognition { + get { + return ResourceManager.GetString("Recognition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recovery. + /// + internal static string Recovery { + get { + return ResourceManager.GetString("Recovery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red eye. + /// + internal static string RedEye { + get { + return ResourceManager.GetString("RedEye", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red-green. + /// + internal static string RedGreen { + get { + return ResourceManager.GetString("RedGreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red week. + /// + internal static string RedWeek { + get { + return ResourceManager.GetString("RedWeek", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Region. + /// + internal static string Region { + get { + return ResourceManager.GetString("Region", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regional language. + /// + internal static string RegionalLanguage { + get { + return ResourceManager.GetString("RegionalLanguage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regional settings properties. + /// + internal static string RegionalSettingsProperties { + get { + return ResourceManager.GetString("RegionalSettingsProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Region and language. + /// + internal static string RegionAndLanguage { + get { + return ResourceManager.GetString("RegionAndLanguage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Region formatting. + /// + internal static string RegionFormatting { + get { + return ResourceManager.GetString("RegionFormatting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RemoteApp and desktop connections. + /// + internal static string RemoteAppAndDesktopConnections { + get { + return ResourceManager.GetString("RemoteAppAndDesktopConnections", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remote Desktop. + /// + internal static string RemoteDesktop { + get { + return ResourceManager.GetString("RemoteDesktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove programs. + /// + internal static string RemovePrograms { + get { + return ResourceManager.GetString("RemovePrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Repair programs. + /// + internal static string RepairPrograms { + get { + return ResourceManager.GetString("RepairPrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resultant Set of Policy. + /// + internal static string ResultantSetOfPolicy { + get { + return ResourceManager.GetString("ResultantSetOfPolicy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scanners and cameras. + /// + internal static string ScannersAndCameras { + get { + return ResourceManager.GetString("ScannersAndCameras", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to schedtasks. + /// + internal static string schedtasks { + get { + return ResourceManager.GetString("schedtasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scheduled. + /// + internal static string Scheduled { + get { + return ResourceManager.GetString("Scheduled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scheduled tasks. + /// + internal static string ScheduledTasks { + get { + return ResourceManager.GetString("ScheduledTasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Screen rotation. + /// + internal static string ScreenRotation { + get { + return ResourceManager.GetString("ScreenRotation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Screen saver. + /// + internal static string ScreenSaver { + get { + return ResourceManager.GetString("ScreenSaver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scroll bars. + /// + internal static string ScrollBars { + get { + return ResourceManager.GetString("ScrollBars", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Scroll Lock. + /// + internal static string ScrollLock { + get { + return ResourceManager.GetString("ScrollLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SDNS. + /// + internal static string Sdns { + get { + return ResourceManager.GetString("Sdns", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searching Windows. + /// + internal static string SearchingWindows { + get { + return ResourceManager.GetString("SearchingWindows", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SecureDNS. + /// + internal static string SecureDNS { + get { + return ResourceManager.GetString("SecureDNS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Security Center. + /// + internal static string SecurityCenter { + get { + return ResourceManager.GetString("SecurityCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Security Configuration and Analysis. + /// + internal static string SecurityConfigurationAndAnalysis { + get { + return ResourceManager.GetString("SecurityConfigurationAndAnalysis", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Security Processor. + /// + internal static string SecurityProcessor { + get { + return ResourceManager.GetString("SecurityProcessor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Security Templates. + /// + internal static string SecurityTemplates { + get { + return ResourceManager.GetString("SecurityTemplates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Services. + /// + internal static string ServicesSnapIn { + get { + return ResourceManager.GetString("ServicesSnapIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session cleanup. + /// + internal static string SessionCleanup { + get { + return ResourceManager.GetString("SessionCleanup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings app. + /// + internal static string SettingsApp { + get { + return ResourceManager.GetString("SettingsApp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set up a kiosk. + /// + internal static string SetUpKiosk { + get { + return ResourceManager.GetString("SetUpKiosk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share across devices. + /// + internal static string ShareAcrossDevices { + get { + return ResourceManager.GetString("ShareAcrossDevices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shared experience settings. + /// + internal static string SharedExperiences { + get { + return ResourceManager.GetString("SharedExperiences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shared Folders. + /// + internal static string SharedFolders { + get { + return ResourceManager.GetString("SharedFolders", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shortcuts. + /// + internal static string Shortcuts { + get { + return ResourceManager.GetString("Shortcuts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to wifi. + /// + internal static string ShortNameWiFi { + get { + return ResourceManager.GetString("ShortNameWiFi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign-in options. + /// + internal static string SignInOptions { + get { + return ResourceManager.GetString("SignInOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign-in options - Dynamic lock. + /// + internal static string SignInOptionsDynamicLock { + get { + return ResourceManager.GetString("SignInOptionsDynamicLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Size. + /// + internal static string Size { + get { + return ResourceManager.GetString("Size", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SMB. + /// + internal static string SMB { + get { + return ResourceManager.GetString("SMB", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sound. + /// + internal static string Sound { + get { + return ResourceManager.GetString("Sound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Speech. + /// + internal static string Speech { + get { + return ResourceManager.GetString("Speech", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Speech recognition. + /// + internal static string SpeechRecognition { + get { + return ResourceManager.GetString("SpeechRecognition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Speech typing. + /// + internal static string SpeechTyping { + get { + return ResourceManager.GetString("SpeechTyping", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start. + /// + internal static string Start { + get { + return ResourceManager.GetString("Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start places. + /// + internal static string StartPlaces { + get { + return ResourceManager.GetString("StartPlaces", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Startup apps. + /// + internal static string StartupApps { + get { + return ResourceManager.GetString("StartupApps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to sticpl.cpl. + /// + internal static string sticpl_cpl { + get { + return ResourceManager.GetString("sticpl.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Storage. + /// + internal static string Storage { + get { + return ResourceManager.GetString("Storage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Storage policies. + /// + internal static string StoragePolicies { + get { + return ResourceManager.GetString("StoragePolicies", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Storage Sense. + /// + internal static string StorageSense { + get { + return ResourceManager.GetString("StorageSense", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sync center. + /// + internal static string SyncCenter { + get { + return ResourceManager.GetString("SyncCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sync your settings. + /// + internal static string SyncYourSettings { + get { + return ResourceManager.GetString("SyncYourSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to sysdm.cpl. + /// + internal static string sysdm_cpl { + get { + return ResourceManager.GetString("sysdm.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System. + /// + internal static string System { + get { + return ResourceManager.GetString("System", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System env vars. + /// + internal static string SystemEnvVars { + get { + return ResourceManager.GetString("SystemEnvVars", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System properties and Add New Hardware wizard. + /// + internal static string SystemPropertiesAndAddNewHardwareWizard { + get { + return ResourceManager.GetString("SystemPropertiesAndAddNewHardwareWizard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System Tools. + /// + internal static string SystemTools { + get { + return ResourceManager.GetString("SystemTools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System variables. + /// + internal static string SystemVariables { + get { + return ResourceManager.GetString("SystemVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tab. + /// + internal static string Tab { + get { + return ResourceManager.GetString("Tab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tablet mode. + /// + internal static string TabletMode { + get { + return ResourceManager.GetString("TabletMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TabletPC.cpl. + /// + internal static string TabletPC_cpl { + get { + return ResourceManager.GetString("TabletPC.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tablet PC settings. + /// + internal static string TabletPcSettings { + get { + return ResourceManager.GetString("TabletPcSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Talk. + /// + internal static string Talk { + get { + return ResourceManager.GetString("Talk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Talk to Cortana. + /// + internal static string TalkToCortana { + get { + return ResourceManager.GetString("TalkToCortana", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Taskbar. + /// + internal static string Taskbar { + get { + return ResourceManager.GetString("Taskbar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Taskbar color. + /// + internal static string TaskbarColor { + get { + return ResourceManager.GetString("TaskbarColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tasks. + /// + internal static string Tasks { + get { + return ResourceManager.GetString("Tasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Task Scheduler. + /// + internal static string TaskScheduler { + get { + return ResourceManager.GetString("TaskScheduler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Team Conferencing. + /// + internal static string TeamConferencing { + get { + return ResourceManager.GetString("TeamConferencing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Team device management. + /// + internal static string TeamDeviceManagement { + get { + return ResourceManager.GetString("TeamDeviceManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to telephon.cpl. + /// + internal static string telephon_cpl { + get { + return ResourceManager.GetString("telephon.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text to speech. + /// + internal static string TextToSpeech { + get { + return ResourceManager.GetString("TextToSpeech", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Themes. + /// + internal static string Themes { + get { + return ResourceManager.GetString("Themes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to themes.cpl. + /// + internal static string themes_cpl { + get { + return ResourceManager.GetString("themes.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to timedate.cpl. + /// + internal static string timedate_cpl { + get { + return ResourceManager.GetString("timedate.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Timeline. + /// + internal static string Timeline { + get { + return ResourceManager.GetString("Timeline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Timeout. + /// + internal static string Timeout { + get { + return ResourceManager.GetString("Timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Touch. + /// + internal static string Touch { + get { + return ResourceManager.GetString("Touch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Touch feedback. + /// + internal static string TouchFeedback { + get { + return ResourceManager.GetString("TouchFeedback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Touchpad. + /// + internal static string Touchpad { + get { + return ResourceManager.GetString("Touchpad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TPM Management. + /// + internal static string TpmManagement { + get { + return ResourceManager.GetString("TpmManagement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Transparency. + /// + internal static string Transparency { + get { + return ResourceManager.GetString("Transparency", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to tritanopia. + /// + internal static string tritanopia { + get { + return ResourceManager.GetString("tritanopia", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Troubleshoot. + /// + internal static string Troubleshoot { + get { + return ResourceManager.GetString("Troubleshoot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TruePlay. + /// + internal static string TruePlay { + get { + return ResourceManager.GetString("TruePlay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Turn screen saver on or off. + /// + internal static string TurnScreenSaverOnOff { + get { + return ResourceManager.GetString("TurnScreenSaverOnOff", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Typing. + /// + internal static string Typing { + get { + return ResourceManager.GetString("Typing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UAC. + /// + internal static string UAC { + get { + return ResourceManager.GetString("UAC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uninstall. + /// + internal static string Uninstall { + get { + return ResourceManager.GetString("Uninstall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uninstall programs. + /// + internal static string UninstallPrograms { + get { + return ResourceManager.GetString("UninstallPrograms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USB. + /// + internal static string Usb { + get { + return ResourceManager.GetString("Usb", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UserAccountControlSettings.exe. + /// + internal static string UserAccountControlSettings_exe { + get { + return ResourceManager.GetString("UserAccountControlSettings.exe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User accounts. + /// + internal static string UserAccounts { + get { + return ResourceManager.GetString("UserAccounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User environment variables. + /// + internal static string UserEnvironmentVariables { + get { + return ResourceManager.GetString("UserEnvironmentVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User env vars. + /// + internal static string UserEnvVars { + get { + return ResourceManager.GetString("UserEnvVars", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User variables. + /// + internal static string UserVariables { + get { + return ResourceManager.GetString("UserVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Version. + /// + internal static string Version { + get { + return ResourceManager.GetString("Version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Video playback. + /// + internal static string VideoPlayback { + get { + return ResourceManager.GetString("VideoPlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Videos. + /// + internal static string Videos { + get { + return ResourceManager.GetString("Videos", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Virtual Desktops. + /// + internal static string VirtualDesktops { + get { + return ResourceManager.GetString("VirtualDesktops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Virus. + /// + internal static string Virus { + get { + return ResourceManager.GetString("Virus", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Voice activation. + /// + internal static string VoiceActivation { + get { + return ResourceManager.GetString("VoiceActivation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Volume. + /// + internal static string Volume { + get { + return ResourceManager.GetString("Volume", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VPN. + /// + internal static string Vpn { + get { + return ResourceManager.GetString("Vpn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wallpaper. + /// + internal static string Wallpaper { + get { + return ResourceManager.GetString("Wallpaper", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Warmer color. + /// + internal static string WarmerColor { + get { + return ResourceManager.GetString("WarmerColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome center. + /// + internal static string WelcomeCenter { + get { + return ResourceManager.GetString("WelcomeCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome screen. + /// + internal static string WelcomeScreen { + get { + return ResourceManager.GetString("WelcomeScreen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to wgpocpl.cpl. + /// + internal static string wgpocpl_cpl { + get { + return ResourceManager.GetString("wgpocpl.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wheel. + /// + internal static string Wheel { + get { + return ResourceManager.GetString("Wheel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wi-Fi. + /// + internal static string WiFi { + get { + return ResourceManager.GetString("WiFi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wi-Fi Calling. + /// + internal static string WiFiCalling { + get { + return ResourceManager.GetString("WiFiCalling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wi-Fi settings. + /// + internal static string WiFiSettings { + get { + return ResourceManager.GetString("WiFiSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Window border. + /// + internal static string WindowBorder { + get { + return ResourceManager.GetString("WindowBorder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Anytime Upgrade. + /// + internal static string WindowsAnytimeUpgrade { + get { + return ResourceManager.GetString("WindowsAnytimeUpgrade", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Anywhere. + /// + internal static string WindowsAnywhere { + get { + return ResourceManager.GetString("WindowsAnywhere", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows CardSpace. + /// + internal static string WindowsCardSpace { + get { + return ResourceManager.GetString("WindowsCardSpace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Defender. + /// + internal static string WindowsDefender { + get { + return ResourceManager.GetString("WindowsDefender", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Firewall. + /// + internal static string WindowsFirewall { + get { + return ResourceManager.GetString("WindowsFirewall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Hello setup - Face. + /// + internal static string WindowsHelloSetupFace { + get { + return ResourceManager.GetString("WindowsHelloSetupFace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Hello setup - Fingerprint. + /// + internal static string WindowsHelloSetupFingerprint { + get { + return ResourceManager.GetString("WindowsHelloSetupFingerprint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Insider Program. + /// + internal static string WindowsInsiderProgram { + get { + return ResourceManager.GetString("WindowsInsiderProgram", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Management Instrumentation. + /// + internal static string WindowsManagementInstrumentation { + get { + return ResourceManager.GetString("WindowsManagementInstrumentation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Mobility Center. + /// + internal static string WindowsMobilityCenter { + get { + return ResourceManager.GetString("WindowsMobilityCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows search. + /// + internal static string WindowsSearch { + get { + return ResourceManager.GetString("WindowsSearch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows search settings. + /// + internal static string WindowsSearchSettings { + get { + return ResourceManager.GetString("WindowsSearchSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Security. + /// + internal static string WindowsSecurity { + get { + return ResourceManager.GetString("WindowsSecurity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update. + /// + internal static string WindowsUpdate { + get { + return ResourceManager.GetString("WindowsUpdate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update - Advanced options. + /// + internal static string WindowsUpdateAdvancedOptions { + get { + return ResourceManager.GetString("WindowsUpdateAdvancedOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update - Check for updates. + /// + internal static string WindowsUpdateCheckForUpdates { + get { + return ResourceManager.GetString("WindowsUpdateCheckForUpdates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update - Restart options. + /// + internal static string WindowsUpdateRestartOptions { + get { + return ResourceManager.GetString("WindowsUpdateRestartOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update - View optional updates. + /// + internal static string WindowsUpdateViewOptionalUpdates { + get { + return ResourceManager.GetString("WindowsUpdateViewOptionalUpdates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Update - View update history. + /// + internal static string WindowsUpdateViewUpdateHistory { + get { + return ResourceManager.GetString("WindowsUpdateViewUpdateHistory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wireless. + /// + internal static string Wireless { + get { + return ResourceManager.GetString("Wireless", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WMI Control. + /// + internal static string WmiControl { + get { + return ResourceManager.GetString("WmiControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Workplace. + /// + internal static string Workplace { + get { + return ResourceManager.GetString("Workplace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Workplace provisioning. + /// + internal static string WorkplaceProvisioning { + get { + return ResourceManager.GetString("WorkplaceProvisioning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to wscui.cpl. + /// + internal static string wscui_cpl { + get { + return ResourceManager.GetString("wscui.cpl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wubi IME settings. + /// + internal static string WubiImeSettings { + get { + return ResourceManager.GetString("WubiImeSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wubi IME settings - UDP. + /// + internal static string WubiImeSettingsUdp { + get { + return ResourceManager.GetString("WubiImeSettingsUdp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Xbox Networking. + /// + internal static string XboxNetworking { + get { + return ResourceManager.GetString("XboxNetworking", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your info. + /// + internal static string YourInfo { + get { + return ResourceManager.GetString("YourInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Zoom. + /// + internal static string Zoom { + get { + return ResourceManager.GetString("Zoom", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx new file mode 100644 index 0000000000..6d07ad71c6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/Properties/Resources.resx @@ -0,0 +1,2085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + About + Area System + + + access.cpl + File name, Should not translated + + + Accessibility Options + Area Control Panel (legacy settings) + + + Accessory apps + Area Privacy + + + Access work or school + Area UserAccounts + + + Account info + Area Privacy + + + Accounts + Area SurfaceHub + + + Action Center + Area Control Panel (legacy settings) + + + Activation + Area UpdateAndSecurity + + + Activity history + Area Privacy + + + Add devices + + + Add Hardware + Area Control Panel (legacy settings) + + + Add or remove programs + Area Control Panel (legacy settings) + + + Add your phone + Area Phone + + + Administrative Tools + Area System + + + Advanced display settings + Area System, only available on devices that support advanced display options + + + Advanced graphics + + + Advertising ID + Area Privacy, Deprecated in Windows 10, version 1809 and later + + + Airplane mode + Area NetworkAndInternet + + + Alt+Tab + Means the key combination "Tabulator+Alt" on the keyboard + + + Alternative names + + + Animations + + + App color + + + Control Panel + Type of the setting is a "(legacy) Control Panel setting" + + + App diagnostics + Area Privacy + + + App features + Area Apps + + + App + Short/modern name for application + + + Microsoft Management Console + + + Apps & Features + Area Apps + + + System settings + Type of the setting is a "Modern Windows settings". We use the same term as used in start menu search at the moment. + + + Apps for websites + Area Apps + + + App volume and device preferences + Area System, Added in Windows 10, version 1903 + + + appwiz.cpl + File name, Should not translated + + + Area + Mean the settings area or settings category + + + Accounts + + + Administrative Tools + Area Control Panel (legacy settings) + + + Appearance and Personalization + + + Apps + + + Bluetooth & devices + Area Bluetooth&Devices in Win11 Settings app + + + Clock and Region + + + Cortana + + + Devices + + + Ease of access + + + Extras + + + Gaming + + + Hardware and Sound + + + Mixed reality + + + Network and Internet + + + Personalization + + + Phone + + + Privacy + + + Programs + + + Security and Maintenance + Area Security and Maintenance in legacy Control Panel app. + + + SurfaceHub + + + System + + + System and Security + + + System Properties + System Properties dialog sysdm.cpl + + + Time and language + + + Update and security + + + User accounts + + + Assigned access + + + Audio + Area EaseOfAccess + + + Audio alerts + + + Audio and speech + Area MixedReality, only available if the Mixed Reality Portal app is installed. + + + Authorization Manager + Name of MMC Snap-In. + + + Automatic file downloads + Area Privacy + + + AutoPlay + Area Device + + + Background + Area Personalization + + + Background Apps + Area Privacy + + + Backup + Area UpdateAndSecurity + + + Backup and Restore + Area Control Panel (legacy settings) + + + Battery Saver + Area System, only available on devices that have a battery, such as a tablet + + + Battery Saver settings + Area System, only available on devices that have a battery, such as a tablet + + + Battery saver usage details + + + Battery use + Area System, only available on devices that have a battery, such as a tablet + + + Biometric Devices + Area Control Panel (legacy settings) + + + BitLocker Drive Encryption + Area Control Panel (legacy settings) + + + Blue light + + + Bluetooth + Area Device + + + Bluetooth and other devices + Page Bluetooth and devices in Win10 Settings app + + + Bluetooth devices + Area Control Panel (legacy settings) + + + Blue-yellow + + + Bopomofo IME + Area TimeAndLanguage + + + bpmf + Should not translated + + + Broadcasting + Area Gaming + + + bthprops.cpl + + + Calendar + Area Privacy + + + Call history + Area Privacy + + + calling + + + Camera + Area Privacy + + + Cangjie IME + Area TimeAndLanguage + + + Caps Lock + Mean the "Caps Lock" key + + + Cellular and SIM + Area NetworkAndInternet + + + Certificates - Current User + Name of MMC Snap-In. + + + Certificates - Local Computer + Name of MMC Snap-In. + + + Change programs + + + Change screen saver + + + Change User Account Control settings + + + Choose which folders appear on Start + Area Personalization + + + Client service for NetWare + Area Control Panel (legacy settings) + + + Clipboard + Area System + + + Closed captions + Area EaseOfAccess + + + collab.cpl + + + Color filters + Area EaseOfAccess + + + Color management + Area Control Panel (legacy settings) + + + Colors + Area Personalization + + + Command + The command to direct start a setting + + + COM-Objects + + + Component Services + Name of MMC Snap-In. + + + Computer Management + Name of MMC Snap-In. + + + Connectable devices + + + Connected Devices + Area Device + + + Connect panel + + + Connect to a wireless audio device + + + Connect to a wireless display + + + Contacts + Area Privacy + + + Copy command + + + Core Isolation + Means the protection of the system core + + + Cortana + Area Cortana + + + Cortana across my devices + Area Cortana + + + Cortana - Language + Area Cortana + + + Create and format hard disk partitions + + + Credential manager + Area Control Panel (legacy settings) + + + Crossdevice + + + Custom devices + + + Dark color + + + Dark mode + + + Data usage + Area NetworkAndInternet + + + Date and time + Area TimeAndLanguage + + + Default apps + Area Apps + + + Default camera + Area Device + + + Default location + Area Control Panel (legacy settings) + + + Default programs + Area Control Panel (legacy settings) + + + Default Save Locations + Area System + + + Windows Defender Firewall with Advanced Security + Name of MMC Snap-In. + + + Delivery Optimization + Area UpdateAndSecurity + + + desk.cpl + File name, Should not translated + + + Desktop themes + Area Control Panel (legacy settings) + + + deuteranopia + Medical: Mean you don't can see red colors + + + Device discovery + + + Device manager + Area Control Panel (legacy settings) + + + Device Manager + Name of MMC Snap-In. + + + Devices + + + Devices and printers + Area Control Panel (legacy settings) + + + DHCP + Should not translated + + + Dial-up + Area NetworkAndInternet + + + Direct access + Area NetworkAndInternet, only available if DirectAccess is enabled + + + Direct open your phone + Area EaseOfAccess + + + Disk Management + Name of MMC Snap-In. + + + Display + Area EaseOfAccess + + + Display properties + Area Control Panel (legacy settings) + + + DNS + Should not translated + + + Documents + Area Privacy + + + Duplicating my display + Area System + + + During these hours + Area System + + + Ease of access center + Area Control Panel (legacy settings) + + + Edit environment variables + Used as AltName on EditSystemEnvironmentVars settings to make both entries available via 'Edit environment variables'. + + + Edition + Means the "Windows Edition" + + + Edit the system environment variables + + + Edit environment variables for your account + + + Email + Area Privacy + + + Email and app accounts + Area UserAccounts + + + Encryption + Area System + + + Environment + Area MixedReality, only available if the Mixed Reality Portal app is installed. + + + Env vars + Short english form. Don't translate! + + + Ethernet + Area NetworkAndInternet + + + Event Viewer + Name of MMC Snap-In. + + + Exploit Protection + + + Extras + Area Extra, , only used for setting of 3rd-Party tools + + + Eye control + Area EaseOfAccess + + + Eye tracker + Area Privacy, requires eyetracker hardware + + + Family and other people + Area UserAccounts + + + Feedback and diagnostics + Area Privacy + + + File system + Area Privacy + + + FindFast + Area Control Panel (legacy settings) + + + findfast.cpl + File name, Should not translated + + + Find My Device + Area UpdateAndSecurity + + + Firewall + + + Firewall.cpl + + + Focus assist - Quiet hours + Area System + + + Focus assist - Quiet moments + Area System + + + Folder options + Area Control Panel (legacy settings) + + + Fonts + Area EaseOfAccess + + + For developers + Area UpdateAndSecurity + + + Game bar + Area Gaming + + + Game controllers + Area Control Panel (legacy settings) + + + Game DVR + Area Gaming + + + Game Mode + Area Gaming + + + Gateway + Should not translated + + + General + Area Privacy + + + Get programs + Area Control Panel (legacy settings) + + + Getting started + Area Control Panel (legacy settings) + + + Glance + Area Personalization, Deprecated in Windows 10, version 1809 and later + + + GPT + + + Graphics settings + Area System + + + Grayscale + + + Green week + Mean you don't can see green colors + + + Group Policy + + + hdwwiz.cpl + + + Headset display + Area MixedReality, only available if the Mixed Reality Portal app is installed. + + + High contrast + Area EaseOfAccess + + + Holographic audio + + + Holographic Environment + + + Holographic Headset + + + Holographic Management + + + Home group + Area Control Panel (legacy settings) + + + ID + MEans The "Windows Identifier" + + + Image + + + Indexing options + Area Control Panel (legacy settings) + + + inetcpl.cpl + File name, Should not translated + + + Infrared + Area Control Panel (legacy settings) + + + Inking and typing + Area Privacy + + + Internet options + Area Control Panel (legacy settings) + + + intl.cpl + File name, Should not translated + + + Inverted colors + + + IP + Should not translated + + + IP Security Monitor + Name of MMC Snap-In. + + + IP Security Policies on Local Computer + Name of MMC Snap-In. + + + irprops.cpl + + + Isolated Browsing + + + Japan IME settings + Area TimeAndLanguage, available if the Microsoft Japan input method editor is installed + + + joy.cpl + File name, Should not translated + + + Joystick properties + Area Control Panel (legacy settings) + + + jpnime + Should not translated + + + Keyboard + Area EaseOfAccess + + + Keypad + + + Keys + + + Language + Area TimeAndLanguage + + + Light color + + + Light mode + + + Local Computer Policy + Name of MMC Snap-In. + + + Local Users and Groups + Name of MMC Snap-In. + + + Location + Area Privacy + + + Lock screen + Area Personalization + + + Magnifier + Area EaseOfAccess + + + Mail - Microsoft Exchange or Windows Messaging + Area Control Panel (legacy settings) + + + main.cpl + File name, Should not translated + + + Manage devices + + + Manage known networks + Area NetworkAndInternet + + + Manage optional features + Area Apps + + + MBR + + + Messaging + Area Privacy + + + Metered connection + + + Microphone + Area Privacy + + + Microsoft Mail Post Office + Area Control Panel (legacy settings) + + + mlcfg32.cpl + File name, Should not translated + + + azman.msc + + + certlm.msc + + + certmgr.msc + + + comexp.msc + + + compmgmt.msc + + + devmgmt.msc + + + diskmgmt.msc + + + eventvwr.msc + + + fsmgmt.msc + + + gpedit.msc + + + lusrmgr.msc + + + mmc.exe + + + perfmon.msc + + + printmanagement.msc + + + rsop.msc + + + secpol.msc + + + services.msc + + + taskschd.msc + + + tpm.msc + + + WF.msc + + + WmiMgmt.msc + + + mmsys.cpl + File name, Should not translated + + + Mobile devices + + + Mobile hotspot + Area NetworkAndInternet + + + modem.cpl + File name, Should not translated + + + Mono + + + More details + Area Cortana + + + Motion + Area Privacy + + + Mouse + Area EaseOfAccess + + + Mouse and touchpad + Area Device + + + Mouse, Fonts, Keyboard, and Printers properties + Area Control Panel (legacy settings) + + + Mouse pointer + Area EaseOfAccess + + + Multimedia properties + Area Control Panel (legacy settings) + + + Multitasking + Area System + + + Narrator + Area EaseOfAccess + + + Navigation bar + Area Personalization + + + ncpa.cpl + + + Nearby sharing settings + Area System + + + netcpl.cpl + File name, Should not translated + + + netsetup.cpl + File name, Should not translated + + + Network + Area NetworkAndInternet + + + Network and sharing center + Area Control Panel (legacy settings) + + + Network connection + Area Control Panel (legacy settings) + + + Network properties + Area Control Panel (legacy settings) + + + Network sessions + + + Network Setup Wizard + Area Control Panel (legacy settings) + + + Network status + Area NetworkAndInternet + + + NFC + Area NetworkAndInternet + + + NFC Transactions + "NFC should not translated" + + + Night light + + + Night light settings + Area System + + + Note + + + Only available when you have connected a mobile device to your device. + + + Only available on devices that support advanced graphics options. + + + Only available on devices that have a battery, such as a tablet. + + + Deprecated in Windows 10, version 1809 (build 17763) and later. + + + Only available if Dial is paired. + + + Only available if DirectAccess is enabled. + + + Only available on devices that support advanced display options. + + + Editing this setting may require administrative privileges. + + + Only present if user is enrolled in WIP. + + + Requires eyetracker hardware. + + + Available if the Microsoft Japan input method editor is installed. + + + Available if the Microsoft Pinyin input method editor is installed. + + + Available if the Microsoft Wubi input method editor is installed. + + + Only available if the Mixed Reality Portal app is installed. + + + Only available on mobile and if the enterprise has deployed a provisioning package. + + + You have to add this snap-in manually. + + + Added in Windows 10, version 1903 (build 18362). + + + Added in Windows 10, version 2004 (build 19041). + + + Only available if "settings apps" are installed, for example, by a 3rd party. + + + Only available if touchpad hardware is present. + + + Only available if the device has a Wi-Fi adapter. + + + Device must be Windows Anywhere-capable. + + + Only available if enterprise has deployed a provisioning package. + + + Notifications + Area Privacy + + + Notifications and actions + Area System + + + Num Lock + Mean the "Num Lock" key + + + nwc.cpl + File name, Should not translated + + + odbccp32.cpl + File name, Should not translated + + + ODBC Data Source Administrator (32-bit) + Area Control Panel (legacy settings) + + + ODBC Data Source Administrator (64-bit) + Area Control Panel (legacy settings) + + + Offline files + Area Control Panel (legacy settings) + + + Offline Maps + Area Apps + + + Offline Maps - Download maps + Area Apps + + + On-Screen + + + Control Panel (Application homepage) + 'Control Panel' is here the name of the legacy settings app. + + + Settings (Application homepage) + 'Settings' is here the name of the modern settings app. + + + OS + Means the "Operating System" + + + Other devices + Area Privacy + + + Other options + Area EaseOfAccess + + + Other users + + + Parental controls + Area Control Panel (legacy settings) + + + Password + + + password.cpl + File name, Should not translated + + + Password properties + Area Control Panel (legacy settings) + + + Pen and input devices + Area Control Panel (legacy settings) + + + Pen and touch + Area Control Panel (legacy settings) + + + Pen and Windows Ink + Area Device + + + People Near Me + Area Control Panel (legacy settings) + + + Performance information and tools + Area Control Panel (legacy settings) + + + Performance Monitor + Name of MMC Snap-In. + + + Permissions and history + Area Cortana + + + Personalization (category) + Area Personalization + + + Phone + Area Phone + + + Phone and modem + Area Control Panel (legacy settings) + + + Phone and modem - Options + Area Control Panel (legacy settings) + + + Phone calls + Area Privacy + + + Phone - Default apps + Area System + + + Phone Link + + + Picture + + + Pictures + Area Privacy + + + Pinyin IME settings + Area TimeAndLanguage, available if the Microsoft Pinyin input method editor is installed + + + Pinyin IME settings - domain lexicon + Area TimeAndLanguage + + + Pinyin IME settings - Key configuration + Area TimeAndLanguage + + + Pinyin IME settings - UDP + Area TimeAndLanguage + + + Playing a game full screen + Area Gaming + + + Searches Windows settings + {Locked="Windows"} + + + Windows settings + + + PNP Device + + + Power and sleep + Area System + + + powercfg.cpl + File name, Should not translated + + + Power options + Area Control Panel (legacy settings) + + + Presentation + + + Printers + Area Control Panel (legacy settings) + + + Printers and scanners + Area Device + + + Printer Spooler + + + Print Management + Name of MMC Snap-In. + + + Print screen + Mean the "Print screen" key + + + Problem reports and solutions + Area Control Panel (legacy settings) + + + Processor + + + Programs and features + Area Control Panel (legacy settings) + + + Projecting to this PC + Area System + + + protanopia + Medical: Mean you don't can see green colors + + + Provisioning + Area UserAccounts, only available if enterprise has deployed a provisioning package + + + Proximity + Area NetworkAndInternet + + + Proxy + Area NetworkAndInternet + + + Quickime + Area TimeAndLanguage + + + Quiet moments game + + + Radios + Area Privacy + + + RAM + Means the Read-Access-Memory (typical the used to inform about the size) + + + Recognition + + + Recovery + Area UpdateAndSecurity + + + Red eye + Mean red eye effect by over-the-night flights + + + Red-green + Mean the weakness you can't differ between red and green colors + + + Red week + Mean you don't can see red colors + + + Region + Area TimeAndLanguage + + + Regional language + Area TimeAndLanguage + + + Regional settings properties + Area Control Panel (legacy settings) + + + Region and language + Area Control Panel (legacy settings) + + + Region formatting + + + RemoteApp and desktop connections + Area Control Panel (legacy settings) + + + Remote Desktop + Area System + + + Remove programs + + + Repair programs + + + Resultant Set of Policy + Name of MMC Snap-In. + + + Scanners and cameras + Area Control Panel (legacy settings) + + + schedtasks + File name, Should not translated + + + Scheduled + + + Scheduled tasks + Area Control Panel (legacy settings) + + + Screen rotation + Area System + + + Screen saver + + + Scroll bars + + + Scroll Lock + Mean the "Scroll Lock" key + + + SDNS + Should not translated + + + Searching Windows + Area Cortana + + + SecureDNS + Should not translated + + + Security Center + Area Control Panel (legacy settings) + + + Security Configuration and Analysis + Name of MMC Snap-In. + + + Security Processor + + + Security Templates + Name of MMC Snap-In. + + + Services + Name of MMC Snap-In. + + + Session cleanup + Area SurfaceHub + + + Settings app + + + Set up a kiosk + Area UserAccounts + + + Share across devices + Area System + + + Shared experience settings + Area System + + + Shared Folders + Name of MMC Snap-In. + + + Shortcuts + + + wifi + dont translate this, is a short term to find entries + + + Sign-in options + Area UserAccounts + + + Sign-in options - Dynamic lock + Area UserAccounts + + + Size + Size for text and symbols + + + SMB + + + Sound + Area System + + + Speech + Area EaseOfAccess + + + Speech recognition + Area Control Panel (legacy settings) + + + Speech typing + + + Start + Area Personalization + + + Start places + + + Startup apps + Area Apps + + + sticpl.cpl + File name, Should not translated + + + Storage + Area System + + + Storage policies + Area System + + + Storage Sense + Area System + + + Sync center + Area Control Panel (legacy settings) + + + Sync your settings + Area UserAccounts + + + sysdm.cpl + File name, Should not translated + + + System + Area Control Panel (legacy settings) + + + System env vars + Short english form. Don't translate! + + + System properties and Add New Hardware wizard + Area Control Panel (legacy settings) + + + System Tools + + + System variables + + + Tab + Means the key "Tabulator" on the keyboard + + + Tablet mode + Area System + + + TabletPC.cpl + + + Tablet PC settings + Area Control Panel (legacy settings) + + + Talk + + + Talk to Cortana + Area Cortana + + + Taskbar + Area Personalization + + + Taskbar color + + + Tasks + Area Privacy + + + Task Scheduler + Name of MMC Snap-In. + + + Team Conferencing + Area SurfaceHub + + + Team device management + Area SurfaceHub + + + telephon.cpl + + + Text to speech + Area Control Panel (legacy settings) + + + Themes + Area Personalization + + + themes.cpl + File name, Should not translated + + + timedate.cpl + File name, Should not translated + + + Timeline + + + Timeout + + + Touch + + + Touch feedback + + + Touchpad + Area Device + + + TPM Management + Name of MMC Snap-In. + + + Transparency + + + tritanopia + Medical: Mean you don't can see yellow and blue colors + + + Troubleshoot + Area UpdateAndSecurity + + + TruePlay + Area Gaming + + + Turn screen saver on or off + + + Typing + Area Device + + + UAC + Short version of 'User account control' + + + Uninstall + Area MixedReality, only available if the Mixed Reality Portal app is installed. + + + Uninstall programs + + + USB + Area Device + + + UserAccountControlSettings.exe + Name of the executable + + + User accounts + Area Control Panel (legacy settings) + + + User environment variables + + + User env vars + Short english form. Don't translate! + + + User variables + + + Version + Means The "Windows Version" + + + Video playback + Area Apps + + + Videos + Area Privacy + + + Virtual Desktops + + + Virus + Means the virus in computers and software + + + Voice activation + Area Privacy + + + Volume + + + VPN + Area NetworkAndInternet + + + Wallpaper + + + Warmer color + + + Welcome center + Area Control Panel (legacy settings) + + + Welcome screen + Area SurfaceHub + + + wgpocpl.cpl + File name, Should not translated + + + Wheel + Area Device + + + Wi-Fi + Area NetworkAndInternet, only available if Wi-Fi calling is enabled + + + Wi-Fi Calling + Area NetworkAndInternet, only available if Wi-Fi calling is enabled + + + Wi-Fi settings + "Wi-Fi" should not translated + + + Window border + + + Windows Anytime Upgrade + Area Control Panel (legacy settings) + + + Windows Anywhere + Area UserAccounts, device must be Windows Anywhere-capable + + + Windows CardSpace + Area Control Panel (legacy settings) + + + Windows Defender + Area Control Panel (legacy settings) + + + Windows Firewall + Area Control Panel (legacy settings) + + + Windows Hello setup - Face + Area UserAccounts + + + Windows Hello setup - Fingerprint + Area UserAccounts + + + Windows Insider Program + Area UpdateAndSecurity + + + Windows Management Instrumentation + + + Windows Mobility Center + Area Control Panel (legacy settings) + + + Windows search + Area Cortana + + + Windows search settings + Area Cortana + + + Windows Security + Area UpdateAndSecurity + + + Windows Update + Area UpdateAndSecurity + + + Windows Update - Advanced options + Area UpdateAndSecurity + + + Windows Update - Check for updates + Area UpdateAndSecurity + + + Windows Update - Restart options + Area UpdateAndSecurity + + + Windows Update - View optional updates + Area UpdateAndSecurity + + + Windows Update - View update history + Area UpdateAndSecurity + + + Wireless + + + WMI Control + Name of MMC Snap-In. + + + Workplace + + + Workplace provisioning + Area UserAccounts + + + wscui.cpl + + + Wubi IME settings + Area TimeAndLanguage, available if the Microsoft Wubi input method editor is installed + + + Wubi IME settings - UDP + Area TimeAndLanguage + + + Xbox Networking + Area Gaming + + + Your info + Area UserAccounts + + + Zoom + Mean zooming of things via a magnifier + + + Open Settings + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json new file mode 100644 index 0000000000..2695b7c1d2 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.json @@ -0,0 +1,1985 @@ +{ + "$schema": "./WindowsSettings.schema.json", + "Settings": [ + { + "Name": "OpenSettingsApp", + "Type": "AppSettingsApp", + "AltNames": [ "SettingsApp", "AppSettingsApp" ], + "Command": "ms-settings:", + "ShowAsFirstResult": true + }, + { + "Name": "OpenControlPanel", + "Type": "AppControlPanel", + "Command": "control.exe", + "ShowAsFirstResult": true + }, + { + "Name": "AccessWorkOrSchool", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "AltNames": [ "Workplace" ], + "Command": "ms-settings:workplace" + }, + { + "Name": "EmailAndAppAccounts", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:emailandaccounts" + }, + { + "Name": "FamilyAndOtherPeople", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "AltNames": [ "OtherUsers" ], + "Command": "ms-settings:otherusers" + }, + { + "Name": "SetUpKiosk", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "AltNames": [ "AssignedAccess" ], + "Command": "ms-settings:assignedaccess" + }, + { + "Name": "SignInOptions", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:signinoptions" + }, + { + "Name": "SignInOptionsDynamicLock", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:signinoptions-dynamiclock" + }, + { + "Name": "SyncYourSettings", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:sync" + }, + { + "Name": "WindowsHelloSetupFace", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:signinoptions-launchfaceenrollment" + }, + { + "Name": "WindowsHelloSetupFingerprint", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:signinoptions-launchfingerprintenrollment" + }, + { + "Name": "YourInfo", + "Areas": [ "AreaAccounts" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:yourinfo" + }, + { + "Name": "AppsAndFeatures", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "AltNames": [ "UninstallPrograms", "RemovePrograms", "ChangePrograms", "RepairPrograms" ], + "Command": "ms-settings:appsfeatures" + }, + { + "Name": "AppFeatures", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:appsfeatures-app" + }, + { + "Name": "AppsForWebsites", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:appsforwebsites" + }, + { + "Name": "DefaultApps", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:defaultapps" + }, + { + "Name": "ManageOptionalFeatures", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:optionalfeatures" + }, + { + "Name": "OfflineMaps", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:maps" + }, + { + "Name": "OfflineMapsDownloadMaps", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:maps-downloadmaps" + }, + { + "Name": "StartupApps", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:startupapps" + }, + { + "Name": "VideoPlayback", + "Areas": [ "AreaApps" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:videoplayback" + }, + { + "Name": "Notifications", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:cortana-notifications" + }, + { + "Name": "MoreDetails", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:cortana-moredetails" + }, + { + "Name": "PermissionsAndHistory", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:cortana-permissions" + }, + { + "Name": "WindowsSearch", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "AltNames": [ "WindowsSearchSettings" ], + "Command": "ms-settings:cortana-windowssearch" + }, + { + "Name": "CortanaLanguage", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "AltNames": [ "Talk" ], + "Command": "ms-settings:cortana-language" + }, + { + "Name": "Cortana", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:cortana" + }, + { + "Name": "TalkToCortana", + "Areas": [ "AreaCortana" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:cortana-talktocortana" + }, + { + "Name": "AutoPlay", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:autoplay" + }, + { + "Name": "BluetoothAndDevices10", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:bluetooth", + "DeprecatedInBuild": 22000 + }, + { + "Name": "Devices", + "Areas": [ "AreaBluetoothAndDevices11" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:bluetooth", + "AltNames": ["ManageDevices", "AddDevices", "BluetoothDevices"], + "IntroducedInBuild": 22000 + }, + { + "Name": "AreaBluetoothAndDevices11", + "Type": "AppSettingsApp", + "Command": "ms-settings:devices", + "AltNames": ["Devices", "BluetoothDevices", "PrintersAndScanners", "PhoneLink", "Camera", "Mouse", "Touchpad", "PenAndWindowsInk", "AutoPlay", "Usb"], + "IntroducedInBuild": 22000 + }, + { + "Name": "ConnectedDevices", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:connecteddevices" + }, + { + "Name": "DefaultCamera", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:camera" + }, + { + "Name": "MouseAndTouchpad", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Note": "NoteTouchpad", + "Command": "ms-settings:mousetouchpad" + }, + { + "Name": "PenAndWindowsInk", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:pen" + }, + { + "Name": "PrintersAndScanners", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:printers" + }, + { + "Name": "Touchpad", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Note": "NoteTouchpad", + "Command": "ms-settings:devices-touchpad" + }, + { + "Name": "Typing", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:typing" + }, + { + "Name": "Usb", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:usb" + }, + { + "Name": "Wheel", + "Areas": [ "AreaDevices" ], + "Type": "AppSettingsApp", + "Note": "NoteDialPaired", + "Command": "ms-settings:wheel" + }, + { + "Name": "Phone", + "Areas": [ "AreaPhone" ], + "Type": "AppSettingsApp", + "AltNames": [ "MobileDevices" ], + "Command": "ms-settings:mobile-devices" + }, + { + "Name": "Audio", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "Mono", "Volume", "AudioAlerts" ], + "Command": "ms-settings:easeofaccess-audio" + }, + { + "Name": "ClosedCaptions", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:easeofaccess-closedcaptioning" + }, + { + "Name": "ColorFilters", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "InvertedColors", "Grayscale", "RedGreen", "BlueYellow", "GreenWeek", "RedWeek", "deuteranopia", "protanopia", "tritanopia" ], + "Command": "ms-settings:easeofaccess-colorfilter" + }, + { + "Name": "MousePointer", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "TouchFeedback" ], + "Command": "ms-settings:easeofaccess-MousePointer" + }, + { + "Name": "Display", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "Transparency", "Animations", "ScrollBars", "Size" ], + "Command": "ms-settings:easeofaccess-display" + }, + { + "Name": "EyeControl", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:easeofaccess-eyecontrol" + }, + { + "Name": "Fonts", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:fonts" + }, + { + "Name": "HighContrast", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:easeofaccess-highcontrast" + }, + { + "Name": "Keyboard", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "PrintScreen", "Shortcuts", "OnScreen", "Keys", "ScrollLock", "CapsLock", "NumLock" ], + "Command": "ms-settings:easeofaccess-keyboard" + }, + { + "Name": "Magnifier", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "Zoom" ], + "Command": "ms-settings:easeofaccess-magnifier" + }, + { + "Name": "Mouse", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "Keypad", "Touch" ], + "Command": "ms-settings:easeofaccess-mouse" + }, + { + "Name": "Narrator", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:easeofaccess-narrator" + }, + { + "Name": "OtherOptions", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:easeofaccess-otheroptions" + }, + { + "Name": "Speech", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppSettingsApp", + "AltNames": [ "Recognition", "Talk" ], + "Command": "ms-settings:easeofaccess-speechrecognition" + }, + { + "Name": "Extras", + "Areas": [ "AreaExtras" ], + "Type": "AppSettingsApp", + "Note": "NoteThirdParty", + "Command": "ms-settings:extras" + }, + { + "Name": "Broadcasting", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:gaming-broadcasting" + }, + { + "Name": "GameBar", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:gaming-gamebar" + }, + { + "Name": "GameDvr", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:gaming-gamedvr" + }, + { + "Name": "GameMode", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:gaming-gamemode" + }, + { + "Name": "PlayingGameFullScreen", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "AltNames": [ "QuietMomentsGame" ], + "Command": "ms-settings:quietmomentsgame" + }, + { + "Name": "TruePlay", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:gaming-trueplay" + }, + { + "Name": "XboxNetworking", + "Areas": [ "AreaGaming" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:gaming-xboxnetworking" + }, + { + "Name": "AudioAndSpeech", + "Areas": [ "AreaMixedReality" ], + "Type": "AppSettingsApp", + "AltNames": [ "HolographicAudio" ], + "Note": "NoteMixedReality", + "Command": "ms-settings:holographic-audio" + }, + { + "Name": "Environment", + "Areas": [ "AreaMixedReality" ], + "Type": "AppSettingsApp", + "AltNames": [ "HolographicEnvironment" ], + "Note": "NoteMixedReality", + "Command": "ms-settings:privacy-holographic-environment" + }, + { + "Name": "HeadsetDisplay", + "Areas": [ "AreaMixedReality" ], + "Type": "AppSettingsApp", + "AltNames": [ "HolographicHeadset" ], + "Note": "NoteMixedReality", + "Command": "ms-settings:holographic-headset" + }, + { + "Name": "Uninstall", + "Areas": [ "AreaMixedReality" ], + "Type": "AppSettingsApp", + "AltNames": [ "HolographicManagement" ], + "Note": "NoteMixedReality", + "Command": "ms-settings:holographic-management" + }, + { + "Name": "AirplaneMode", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-airplanemode" + }, + { + "Name": "Proximity", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:proximity" + }, + { + "Name": "CellularAndSim", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-cellular" + }, + { + "Name": "DataUsage", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:datausage" + }, + { + "Name": "DialUp", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-dialup" + }, + { + "Name": "DirectAccess", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Note": "NoteDirectAccess", + "Command": "ms-settings:network-directaccess" + }, + { + "Name": "Ethernet", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "DNS", "Sdns", "SecureDNS", "Gateway", "Dhcp", "Ip" ], + "Command": "ms-settings:network-ethernet" + }, + { + "Name": "ManageKnownNetworks", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "WiFiSettings", "ShortNameWiFi" ], + "Note": "NoteWiFiAdapter", + "Command": "ms-settings:network-wifisettings" + }, + { + "Name": "MobileHotspot", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-mobilehotspot" + }, + { + "Name": "NFC", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "NFCTransactions" ], + "Command": "ms-settings:nfctransactions" + }, + { + "Name": "Proxy", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-proxy" + }, + { + "Name": "NetworkStatus", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-status" + }, + { + "Name": "Network", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "DNS", "Sdns", "SecureDNS", "Gateway", "Dhcp", "Ip" ], + "Command": "ms-settings:network" + }, + { + "Name": "Vpn", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:network-vpn" + }, + { + "Name": "WiFi", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "Wireless", "MeteredConnection", "ShortNameWiFi" ], + "Note": "NoteWiFiAdapter", + "Command": "ms-settings:network-wifi" + }, + { + "Name": "WiFiCalling", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppSettingsApp", + "AltNames": [ "ShortNameWiFi" ], + "Note": "NoteWiFiAdapter", + "Command": "ms-settings:network-wificalling" + }, + { + "Name": "Background", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "AltNames": [ "Wallpaper", "Picture", "Image" ], + "Command": "ms-settings:personalization-background" + }, + { + "Name": "ChooseWhichFoldersAppearOnStart", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "AltNames": [ "StartPlaces" ], + "Command": "ms-settings:personalization-start-places" + }, + { + "Name": "Colors", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "AltNames": [ "DarkMode", "LightMode", "DarkColor", "LightColor", "AppColor", "TaskbarColor", "WindowBorder" ], + "Command": "ms-settings:colors" + }, + { + "Name": "Glance", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:personalization-glance" + }, + { + "Name": "LockScreen", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "AltNames": [ "Image", "Picture", "ScreenSaver" ], + "Command": "ms-settings:lockscreen" + }, + { + "Name": "NavigationBar", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:personalization-navbar" + }, + { + "Name": "PersonalizationCategory", + "Type": "AppSettingsApp", + "Command": "ms-settings:personalization" + }, + { + "Name": "Start", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:personalization-start" + }, + { + "Name": "Taskbar", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:taskbar" + }, + { + "Name": "Themes", + "Areas": [ "AreaPersonalization" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:themes" + }, + { + "Name": "AddYourPhone", + "Areas": [ "AreaPhone" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:mobile-devices-addphone", + "Note": "NoteAddYourPhone" + }, + { + "Name": "DirectOpenYourPhone", + "Areas": [ "AreaPhone" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:mobile-devices-addphone-direct", + "Note": "NoteAddYourPhone" + }, + { + "Name": "AccessoryApps", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:privacy-accessoryapps" + }, + { + "Name": "AccountInfo", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-accountinfo" + }, + { + "Name": "ActivityHistory", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-activityhistory" + }, + { + "Name": "AdvertisingId", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:privacy-advertisingid" + }, + { + "Name": "AppDiagnostics", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-appdiagnostics" + }, + { + "Name": "AutomaticFileDownloads", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-automaticfiledownloads" + }, + { + "Name": "BackgroundApps", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-backgroundapps" + }, + { + "Name": "Calendar", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-calendar" + }, + { + "Name": "CallHistory", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-callhistory" + }, + { + "Name": "Camera", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-webcam" + }, + { + "Name": "Contacts", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-contacts" + }, + { + "Name": "Documents", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-documents" + }, + { + "Name": "Email", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-email" + }, + { + "Name": "EyeTracker", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Note": "NoteEyetrackerHardware", + "Command": "ms-settings:privacy-eyetracker" + }, + { + "Name": "FeedbackAndDiagnostics", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-feedback" + }, + { + "Name": "FileSystem", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-broadfilesystemaccess" + }, + { + "Name": "General", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-general" + }, + { + "Name": "InkingAndTyping", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "AltNames": [ "SpeechTyping" ], + "Command": "ms-settings:privacy-speechtyping" + }, + { + "Name": "Location", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-location" + }, + { + "Name": "Messaging", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-messaging" + }, + { + "Name": "Microphone", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-microphone" + }, + { + "Name": "Motion", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-motion" + }, + { + "Name": "Notifications", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-notifications" + }, + { + "Name": "OtherDevices", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "AltNames": [ "CustomDevices" ], + "Command": "ms-settings:privacy-customdevices" + }, + { + "Name": "PhoneCalls", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-phonecalls" + }, + { + "Name": "Pictures", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-pictures" + }, + { + "Name": "Radios", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-radios" + }, + { + "Name": "Speech", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-speech" + }, + { + "Name": "Tasks", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-tasks" + }, + { + "Name": "Videos", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-videos" + }, + { + "Name": "VoiceActivation", + "Areas": [ "AreaPrivacy" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:privacy-voiceactivation" + }, + { + "Name": "Accounts", + "Areas": [ "AreaSurfaceHub" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:surfacehub-accounts" + }, + { + "Name": "SessionCleanup", + "Areas": [ "AreaSurfaceHub" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:surfacehub-sessioncleanup" + }, + { + "Name": "TeamConferencing", + "Areas": [ "AreaSurfaceHub" ], + "Type": "AppSettingsApp", + "AltNames": [ "calling" ], + "Command": "ms-settings:surfacehub-calling" + }, + { + "Name": "TeamDeviceManagement", + "Areas": [ "AreaSurfaceHub" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:surfacehub-devicemanagenent" + }, + { + "Name": "WelcomeScreen", + "Areas": [ "AreaSurfaceHub" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:surfacehub-welcome" + }, + { + "Name": "About", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Ram", "Processor", "Os", "Id", "Edition", "Version" ], + "Command": "ms-settings:about" + }, + { + "Name": "AdvancedDisplaySettings", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Note": "NoteDisplayGraphics", + "Command": "ms-settings:display-advanced" + }, + { + "Name": "AppVolumeAndDevicePreferences", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "IntroducedInBuild": 18362, + "Note": "NoteSince18362", + "Command": "ms-settings:apps-volume" + }, + { + "Name": "BatterySaver", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Note": "NoteBattery", + "Command": "ms-settings:batterysaver" + }, + { + "Name": "BatterySaverSettings", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Note": "NoteBattery", + "Command": "ms-settings:batterysaver-settings" + }, + { + "Name": "BatteryUse", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "BatterySaverUsageDetails" ], + "Note": "NoteBattery", + "Command": "ms-settings:batterysaver-usagedetails" + }, + { + "Name": "Clipboard", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:clipboard" + }, + { + "Name": "Display", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "NightLight", "BlueLight", "WarmerColor", "RedEye" ], + "Command": "ms-settings:display" + }, + { + "Name": "DefaultSaveLocations", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:savelocations" + }, + { + "Name": "ScreenRotation", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:screenrotation" + }, + { + "Name": "DuplicatingMyDisplay", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Presentation" ], + "Command": "ms-settings:quietmomentspresentation" + }, + { + "Name": "DuringTheseHours", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Scheduled" ], + "Command": "ms-settings:quietmomentsscheduled" + }, + { + "Name": "Encryption", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:deviceencryption" + }, + { + "Name": "FocusAssistQuietHours", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:quiethours" + }, + { + "Name": "FocusAssistQuietMoments", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:quietmomentshome" + }, + { + "Name": "GraphicsSettings", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "AdvancedGraphics" ], + "Note": "NoteAdvancedGraphics", + "Command": "ms-settings:display-advancedgraphics" + }, + { + "Name": "Messaging", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:messaging" + }, + { + "Name": "Multitasking", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Timeline", "Tab", "AltAndTab", "VirtualDesktops" ], + "Command": "ms-settings:multitasking" + }, + { + "Name": "NightLightSettings", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:nightlight" + }, + { + "Name": "PhoneDefaultApps", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:phone-defaultapps" + }, + { + "Name": "ProjectingToThisPc", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:project" + }, + { + "Name": "SharedExperiences", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Crossdevice", "NearbyShareSettings", "ShareAcrossDevices" ], + "Command": "ms-settings:crossdevice" + }, + { + "Name": "NearbyShareSettings", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Crossdevice", "ShareAcrossDevices", "SharedExperiences" ], + "Command": "ms-settings:crossdevice" + }, + { + "Name": "ShareAcrossDevices", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "Crossdevice", "NearbyShareSettings", "SharedExperiences" ], + "Command": "ms-settings:crossdevice" + }, + { + "Name": "TabletMode", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:tabletmode" + }, + { + "Name": "Taskbar", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:taskbar" + }, + { + "Name": "NotificationsAndActions", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:notifications" + }, + { + "Name": "RemoteDesktop", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:remotedesktop" + }, + { + "Name": "Phone", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "DeprecatedInBuild": 17763, + "Note": "NoteDeprecated17763", + "Command": "ms-settings:phone" + }, + { + "Name": "PowerAndSleep", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:powersleep" + }, + { + "Name": "Sound", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:sound" + }, + { + "Name": "StorageSense", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:storagesense" + }, + { + "Name": "StoragePolicies", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:storagepolicies" + }, + { + "Name": "DateAndTime", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:dateandtime" + }, + { + "Name": "JapanImeSettings", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "AltNames": [ "jpnime" ], + "Note": "NoteImeJapan", + "Command": "ms-settings:regionlanguage-jpnime" + }, + { + "Name": "Region", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "AltNames": [ "RegionFormatting" ], + "Command": "ms-settings:regionformatting" + }, + { + "Name": "Keyboard", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:keyboard" + }, + { + "Name": "RegionalLanguage", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage" + }, + { + "Name": "BopomofoIme", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "AltNames": [ "bpmf" ], + "Command": "ms-settings:regionlanguage-bpmfime" + }, + { + "Name": "CangjieIme", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-cangjieime" + }, + { + "Name": "PinyinImeSettingsDomainLexicon", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-chsime-pinyin-domainlexicon" + }, + { + "Name": "PinyinImeSettingsKeyConfiguration", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-chsime-pinyin-keyconfig" + }, + { + "Name": "PinyinImeSettingsUdp", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-chsime-pinyin-udp" + }, + { + "Name": "WubiImeSettingsUdp", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-chsime-wubi-udp" + }, + { + "Name": "Quickime", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:regionlanguage-quickime" + }, + { + "Name": "PinyinImeSettings", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Note": "NoteImePinyin", + "Command": "ms-settings:regionlanguage-chsime-pinyin" + }, + { + "Name": "Speech", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:speech" + }, + { + "Name": "WubiImeSettings", + "Areas": [ "AreaTimeAndLanguage" ], + "Type": "AppSettingsApp", + "Note": "NoteImeWubi", + "Command": "ms-settings:regionlanguage-chsime-wubi" + }, + { + "Name": "Activation", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:activation" + }, + { + "Name": "Backup", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:backup" + }, + { + "Name": "DeliveryOptimization", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:delivery-optimization" + }, + { + "Name": "FindMyDevice", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:findmydevice" + }, + { + "Name": "ForDevelopers", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:developers" + }, + { + "Name": "Recovery", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:recovery" + }, + { + "Name": "Troubleshoot", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:troubleshoot" + }, + { + "Name": "WindowsSecurity", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "AltNames": [ "WindowsDefender", "Firewall", "Virus", "CoreIsolation", "SecurityProcessor", "IsolatedBrowsing", "ExploitProtection" ], + "Command": "ms-settings:windowsdefender" + }, + { + "Name": "WindowsInsiderProgram", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Note": "NoteEnrolledWIP", + "Command": "ms-settings:windowsinsider" + }, + { + "Name": "WindowsUpdate", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate" + }, + { + "Name": "WindowsUpdateCheckForUpdates", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate-action", + "DeprecatedInBuild": 22000 + }, + { + "Name": "WindowsUpdateCheckForUpdates", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate", + "IntroducedInBuild": 22000 + }, + { + "Name": "WindowsUpdateAdvancedOptions", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate-options" + }, + { + "Name": "WindowsUpdateRestartOptions", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate-restartoptions" + }, + { + "Name": "WindowsUpdateViewUpdateHistory", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "Command": "ms-settings:windowsupdate-history" + }, + { + "Name": "WindowsUpdateViewOptionalUpdates", + "Areas": [ "AreaUpdateAndSecurity" ], + "Type": "AppSettingsApp", + "IntroducedInBuild": 19041, + "Note": "NoteSince19041", + "Command": "ms-settings:windowsupdate-optionalupdates" + }, + { + "Name": "WorkplaceProvisioning", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppSettingsApp", + "Note": "NoteWorkplaceProvisioning", + "Command": "ms-settings:workplace-provisioning" + }, + { + "Name": "Provisioning", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppSettingsApp", + "Note": "NoteMobileProvisioning", + "Command": "ms-settings:provisioning" + }, + { + "Name": "WindowsAnywhere", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppSettingsApp", + "Note": "NoteWindowsAnywhere", + "Command": "ms-settings:windowsanywhere" + }, + { + "Name": "AccessibilityOptions", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppControlPanel", + "AltNames": [ "access.cpl" ], + "Command": "control access.cpl" + }, + { + "Name": "ActionCenter", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "wscui.cpl" ], + "Command": "control /name Microsoft.ActionCenter" + }, + { + "Name": "AddHardware", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.AddHardware" + }, + { + "Name": "AddRemovePrograms", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "appwiz.cpl", "UninstallPrograms", "ChangePrograms", "RepairPrograms" ], + "Command": "control appwiz.cpl" + }, + { + "Name": "AdministrativeTools", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.AdministrativeTools" + }, + { + "Name": "AutoPlay", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.AutoPlay" + }, + { + "Name": "BackupAndRestore", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.BackupAndRestore" + }, + { + "Name": "BiometricDevices", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.BiometricDevices" + }, + { + "Name": "BitLockerDriveEncryption", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.BitLockerDriveEncryption" + }, + { + "Name": "BluetoothDevices", + "Areas": [ "AreaHardwareAndSound", "bthprops.cpl" ], + "Type": "AppControlPanel", + "Command": "control /bthprops.cpl" + }, + { + "Name": "ColorManagement", + "Areas": [ "AreaAppearanceAndPersonalization" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.ColorManagement" + }, + { + "Name": "CredentialManager", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppControlPanel", + "AltNames": [ "Password" ], + "Command": "control /name Microsoft.CredentialManager" + }, + { + "Name": "ClientServiceForNetWare", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "AltNames": [ "nwc.cpl" ], + "Command": "control nwc.cpl" + }, + { + "Name": "DateAndTime", + "Areas": [ "AreaClockAndRegion" ], + "Type": "AppControlPanel", + "AltNames": [ "timedate.cpl" ], + "Command": "control /name Microsoft.DateAndTime" + }, + { + "Name": "DefaultLocation", + "Areas": [ "AreaClockAndRegion" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.DefaultLocation" + }, + { + "Name": "DefaultPrograms", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.DefaultPrograms" + }, + { + "Name": "DeviceManager", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "hdwwiz.cpl" ], + "Command": "control /name Microsoft.DeviceManager" + }, + { + "Name": "DevicesAndPrinters", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.DevicesAndPrinters", + "DeprecatedInBuild": 22000 + }, + { + "Name": "DevicesAndPrinters", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "explorer.exe shell:::{A8A91A66-3A7D-4424-8D24-04E180695C7A}", + "IntroducedInBuild": 22000 + }, + { + "Name": "EaseOfAccessCenter", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.EaseOfAccessCenter" + }, + { + "Name": "FolderOptions", + "Areas": [ "AreaAppearanceAndPersonalization" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.FolderOptions" + }, + { + "Name": "Fonts", + "Areas": [ "AreaAppearanceAndPersonalization" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.Fonts" + }, + { + "Name": "GameControllers", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.GameControllers" + }, + { + "Name": "GetPrograms", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.GetPrograms" + }, + { + "Name": "GettingStarted", + "Type": "AppControlPanel", + "Command": "control /name Microsoft.GettingStarted" + }, + { + "Name": "HomeGroup", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.HomeGroup" + }, + { + "Name": "IndexingOptions", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.IndexingOptions" + }, + { + "Name": "Infrared", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "irprops.cpl" ], + "Command": "control /name Microsoft.Infrared" + }, + { + "Name": "InternetOptions", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "inetcpl.cpl" ], + "Command": "control /name Microsoft.InternetOptions" + }, + { + "Name": "MailMicrosoftExchangeOrWindowsMessaging", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "mlcfg32.cpl" ], + "Command": "control mlcfg32.cpl" + }, + { + "Name": "Mouse", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.Mouse" + }, + { + "Name": "NetworkAndSharingCenter", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.NetworkAndSharingCenter" + }, + { + "Name": "NetworkConnection", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "ncpa.cpl" ], + "Command": "control netconnections" + }, + { + "Name": "NetworkSetupWizard", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "netsetup.cpl" ], + "Command": "control netsetup.cpl" + }, + { + "Name": "OdbcDataSourceAdministrator32Bit", + "Areas": [ "AreaSystemAndSecurity", "AreaAdministrativeTools" ], + "Type": "AppControlPanel", + "AltNames": [ "odbccp32.cpl" ], + "Command": "%windir%/syswow64/odbcad32.exe" + }, + { + "Name": "OdbcDataSourceAdministrator64Bit", + "Areas": [ "AreaSystemAndSecurity", "AreaAdministrativeTools" ], + "Type": "AppControlPanel", + "Command": "%windir%/system32/odbcad32.exe" + }, + { + "Name": "OfflineFiles", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.OfflineFiles" + }, + { + "Name": "ParentalControls", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.ParentalControls" + }, + { + "Name": "PenAndInputDevices", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.PenAndInputDevices" + }, + { + "Name": "PenAndTouch", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.PenAndTouch" + }, + { + "Name": "PeopleNearMe", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.PeopleNearMe" + }, + { + "Name": "PerformanceInformationAndTools", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.PerformanceInformationAndTools" + }, + { + "Name": "PhoneAndModemOptions", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "modem.cpl", "telephon.cpl" ], + "Command": "control /name Microsoft.PhoneAndModemOptions" + }, + { + "Name": "PhoneAndModem", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "modem.cpl", "telephon.cpl" ], + "Command": "control /name Microsoft.PhoneAndModem" + }, + { + "Name": "PowerOptions", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "powercfg.cpl" ], + "Command": "control /name Microsoft.PowerOptions" + }, + { + "Name": "Printers", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.Printers" + }, + { + "Name": "ProblemReportsAndSolutions", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.ProblemReportsAndSolutions" + }, + { + "Name": "ProgramsAndFeatures", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.ProgramsAndFeatures" + }, + { + "Name": "Recovery", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.Recovery" + }, + { + "Name": "RegionAndLanguage", + "Areas": [ "AreaClockAndRegion" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.RegionAndLanguage" + }, + { + "Name": "RemoteAppAndDesktopConnections", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.RemoteAppAndDesktopConnections" + }, + { + "Name": "ScannersAndCameras", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "sticpl.cpl" ], + "Command": "control /name Microsoft.ScannersAndCameras" + }, + { + "Name": "ScheduledTasks", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "schedtasks" ], + "Command": "control schedtasks" + }, + { + "Name": "SecurityCenter", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.SecurityCenter" + }, + { + "Name": "Sound", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.Sound" + }, + { + "Name": "SpeechRecognition", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.SpeechRecognition" + }, + { + "Name": "SyncCenter", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.SyncCenter" + }, + { + "Name": "System", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "sysdm.cpl" ], + "Command": "control sysdm.cpl" + }, + { + "Name": "TabletPcSettings", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "TabletPC.cpl" ], + "Command": "control /name Microsoft.TabletPCSettings" + }, + { + "Name": "TextToSpeech", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.TextToSpeech" + }, + { + "Name": "UserAccounts", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.UserAccounts" + }, + { + "Name": "WelcomeCenter", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.WelcomeCenter" + }, + { + "Name": "WindowsAnytimeUpgrade", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.WindowsAnytimeUpgrade" + }, + { + "Name": "WindowsCardSpace", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.CardSpace" + }, + { + "Name": "WindowsDefender", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.WindowsDefender" + }, + { + "Name": "WindowsFirewall", + "Areas": [ "AreaSystemAndSecurity", "Firewall.cpl" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.WindowsFirewall" + }, + { + "Name": "WindowsMobilityCenter", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "Command": "control /name Microsoft.MobilityCenter" + }, + { + "Name": "DisplayProperties", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "desk.cpl" ], + "Command": "control Desk.cpl" + }, + { + "Name": "FindFast", + "Areas": [ "AreaSystemAndSecurity" ], + "Type": "AppControlPanel", + "AltNames": [ "findfast.cpl" ], + "Command": "control FindFast.cpl" + }, + { + "Name": "RegionalSettingsProperties", + "Areas": [ "AreaEaseOfAccess" ], + "Type": "AppControlPanel", + "AltNames": [ "intl.cpl" ], + "Command": "control Intl.cpl" + }, + { + "Name": "JoystickProperties", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "joy.cpl" ], + "Command": "control Joy.cpl" + }, + { + "Name": "MouseFontsKeyboardAndPrintersProperties", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "main.cpl" ], + "Command": "control Main.cpl" + }, + { + "Name": "MultimediaProperties", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "mmsys.cpl" ], + "Command": "control Mmsys.cpl" + }, + { + "Name": "NetworkProperties", + "Areas": [ "AreaNetworkAndInternet" ], + "Type": "AppControlPanel", + "AltNames": [ "netcpl.cpl" ], + "Command": "control Netcpl.cpl" + }, + { + "Name": "PasswordProperties", + "Areas": [ "AreaUserAccounts" ], + "Type": "AppControlPanel", + "AltNames": [ "password.cpl" ], + "Command": "control Password.cpl" + }, + { + "Name": "SystemPropertiesAndAddNewHardwareWizard", + "Areas": [ "AreaHardwareAndSound" ], + "Type": "AppControlPanel", + "AltNames": [ "sysdm.cpl" ], + "Command": "control Sysdm.cpl" + }, + { + "Name": "DesktopThemes", + "Areas": [ "AreaAppearanceAndPersonalization" ], + "Type": "AppControlPanel", + "AltNames": [ "themes.cpl" ], + "Command": "control Themes.cpl" + }, + { + "Name": "MicrosoftMailPostOffice", + "Areas": [ "AreaPrograms" ], + "Type": "AppControlPanel", + "AltNames": [ "wgpocpl.cpl" ], + "Command": "control Wgpocpl.cpl" + }, + { + "Name": "ChangeUACSettings", + "Areas": [ "AreaSystemAndSecurity", "AreaSecurityAndMaintenance" ], + "Type": "AppControlPanel", + "AltNames": [ "UserAccountControlSettings.exe", "UserAccounts", "UAC" ], + "Command": "UserAccountControlSettings.exe", + "Note": "NoteEditingRequireAdminPrivileges" + }, + { + "Name": "EditSystemEnvironmentVariables", + "Areas": [ "AreaSystemAndSecurity", "AreaSystemPropertiesAdvanced" ], + "Type": "AppControlPanel", + "AltNames": [ "EditEnvironmentVariables", "SystemVariables", "EnvVars", "SystemEnvVars", "sysdm.cpl" ], + "Command": "SystemPropertiesAdvanced.exe", + "Note": "NoteEditingRequireAdminPrivileges" + }, + { + "Name": "EditUserEnvironmentVariables", + "Areas": [ "AreaSystemAndSecurity", "AreaSystemPropertiesAdvanced" ], + "Type": "AppControlPanel", + "AltNames": [ "UserEnvironmentVariables", "UserVariables", "EnvVars", "UserEnvVars", "sysdm.cpl" ], + "Command": "rundll32.exe sysdm.cpl,EditEnvironmentVariables" + }, + { + "Name": "ChangeScreenSaver", + "Type": "AppControlPanel", + "AltNames": [ "TurnScreenSaverOnOff", "Timeout", "desk.cpl" ], + "Command": "control desk.cpl,,@screensaver" + }, + { + "Name": "ConnectWirelessDisplay", + "Areas": [ "AreaSystem" ], + "Type": "AppSettingsApp", + "AltNames": [ "ConnectPanel", "ConnectableDevices", "ConnectWirelessAudio", "DeviceDiscovery" ], + "Command": "ms-settings-connectabledevices:devicediscovery" + }, + { + "Name": "AppMMC", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe" ], + "Command": "mmc.exe", + "ShowAsFirstResult" : true + }, + { + "Name": "AuthorizationManager", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_azman" ], + "Command": "azman.msc" + }, + { + "Name": "CertificatesCurrentUser", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_certmgr" ], + "Command": "certmgr.msc" + }, + { + "Name": "CertificatesLocalComputer", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_certlm" ], + "Command": "certlm.msc" + }, + { + "Name": "ComponentServices", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_comexp", "ComObjects" ], + "Command": "comexp.msc" + }, + { + "Name": "ComputerManagement", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_compmgmt", "SystemTools", "TaskScheduler", "EventViewer", "SharedFolders", "NetworkSessions", "SMB", "LocalUsersAndGroups", "PerformanceMonitor", "DeviceManager", "PnpDevice", "Storage", "DiskManagement", "CreateAndFormatHardDiskPartitions", "GPT", "MBR", "ServicesSnapIn", "WmiControl", "WindowsManagementInstrumentation" ], + "Command": "compmgmt.msc" + }, + { + "Name": "DeviceManager", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_devmgmt", "PnpDevice" ], + "Command": "devmgmt.msc" + }, + { + "Name": "DiskManagement", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_diskmgmt", "Storage", "CreateAndFormatHardDiskPartitions", "GPT", "MBR" ], + "Command": "diskmgmt.msc" + }, + { + "Name": "EventViewer", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_eventvwr" ], + "Command": "eventvwr.msc" + }, + { + "Name": "LocalGroupPolicy", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_gpedit", "GroupPolicy" ], + "Command": "gpedit.msc" + }, + { + "Name": "IpSecurityMonitor", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe" ], + "Command": "mmc.exe", + "Note": "NoteNoMscFileExist" + }, + { + "Name": "IpSecurityPoliciesOnLocalComputer", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe" ], + "Command": "mmc.exe", + "Note": "NoteNoMscFileExist" + }, + { + "Name": "LocalUsersAndGroups", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_lusrmgr" ], + "Command": "lusrmgr.msc" + }, + { + "Name": "PerformanceMonitor", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_perfmon" ], + "Command": "perfmon.msc" + }, + { + "Name": "PrintManagement", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_printmanagement", "PrinterSpooler" ], + "Command": "printmanagement.msc" + }, + { + "Name": "ResultantSetOfPolicy", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_rsop" ], + "Command": "rsop.msc" + }, + { + "Name": "SecurityConfigurationAndAnalysis", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_secpol" ], + "Command": "secpol.msc" + }, + { + "Name": "SecurityTemplates", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe" ], + "Command": "mmc.exe", + "Note": "NoteNoMscFileExist" + }, + { + "Name": "ServicesSnapIn", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_services" ], + "Command": "services.msc" + }, + { + "Name": "SharedFolders", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_fsmgmt", "NetworkSessions" ], + "Command": "fsmgmt.msc" + }, + { + "Name": "TaskScheduler", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_taskschd" ], + "Command": "taskschd.msc" + }, + { + "Name": "TpmManagement", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_tpm" ], + "Command": "tpm.msc" + }, + { + "Name": "DefenderFirewallAdvancedSecurity", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_wf" ], + "Command": "wf.msc" + }, + { + "Name": "WmiControl", + "Type": "AppMMC", + "AltNames": [ "MMC_mmcexe", "MMC_wmimgmt", "WindowsManagementInstrumentation" ], + "Command": "wmimgmt.msc" + } + ] +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json new file mode 100644 index 0000000000..a60e5c5ffd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettings.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "additionalProperties": false, + "required": [ "Settings" ], + "properties": { + "$schema": { + "description": "Path to the schema file.", + "type": "string" + }, + "Settings": { + "description": "A list with all possible windows settings.", + "type": "array", + "items": { + "additionalProperties": false, + "required": [ "Name", "Command", "Type" ], + "type": "object", + "properties": { + "Name": { + "description": "The name of this setting.", + "type": "string" + }, + "Areas": { + "description": "A list of areas of this setting", + "type": "array", + "items": { + "description": "A area of this setting", + "type": "string", + "pattern": "^Area" + } + }, + "Type": { + "description": "The type of this setting.", + "type": "string", + "pattern": "^App" + }, + "AltNames": { + "description": "A list with alternative names for this setting", + "type": "array", + "items": { + "description": "A alternative name for this setting", + "type": "string" + } + }, + "Command": { + "description": "The command for this setting.", + "type": "string" + }, + "Note": { + "description": "A additional note for this setting.", + "type": "string", + "pattern": "^Note" + }, + "DeprecatedInBuild": { + "description": "The Windows build since this settings is not longer present.", + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "IntroducedInBuild": { + "description": "The minimum need Windows build for this setting.", + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "ShowAsFirstResult": { + "description": "Use a higher score as normal for this setting to show it as one of the first results.", + "type": "boolean" + } + } + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs new file mode 100644 index 0000000000..1cd26ca577 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs @@ -0,0 +1,44 @@ +// 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.WindowsSettings.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsSettings; + +public partial class WindowsSettingsCommandsProvider : CommandProvider +{ + private readonly CommandItem _searchSettingsListItem; + +#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + private readonly WindowsSettings.Classes.WindowsSettings? _windowsSettings; +#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. + + public WindowsSettingsCommandsProvider() + { + Id = "Windows.Settings"; + DisplayName = $"Windows Settings"; + Icon = IconHelpers.FromRelativePath("Assets\\WindowsSettings.svg"); + + _windowsSettings = JsonSettingsListHelper.ReadAllPossibleSettings(); + _searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings)) + { + Title = "Windows Settings", + Subtitle = "Navigate to specific Windows settings", + }; + + UnsupportedSettingsHelper.FilterByBuild(_windowsSettings); + + TranslationHelper.TranslateAllSettings(_windowsSettings); + WindowsSettingsPathHelper.GenerateSettingsPathValues(_windowsSettings); + } + + public override ICommandItem[] TopLevelCommands() + { + return [ + _searchSettingsListItem + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.png b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.png new file mode 100644 index 0000000000..d330e93d4c Binary files /dev/null and b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.png differ diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.svg b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.svg new file mode 100644 index 0000000000..3a86f6d867 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Assets/WindowsTerminal.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs new file mode 100644 index 0000000000..fd8354495a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Commands; + +internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand +{ + private readonly string _id; + private readonly string _profile; + private readonly bool _openNewTab; + private readonly bool _openQuake; + + internal LaunchProfileAsAdminCommand(string id, string profile, bool openNewTab, bool openQuake) + { + this._id = id; + this._profile = profile; + this._openNewTab = openNewTab; + this._openQuake = openQuake; + + this.Name = Resources.launch_profile_as_admin; + this.Icon = new IconInfo("\xE7EF"); // Admin icon + } + + private void LaunchElevated(string id, string profile) + { + try + { + var path = "shell:AppsFolder\\" + id; + var arguments = TerminalHelper.GetArguments(profile, _openNewTab, _openQuake); + + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = path, + Arguments = arguments, + UseShellExecute = true, + Verb = "runas", + }; + + System.Diagnostics.Process.Start(startInfo); + } +#pragma warning disable IDE0059, CS0168, SA1005 + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + //var name = "Plugin: " + Resources.plugin_name; + //var message = Resources.run_terminal_failed; + //Log.Exception("Failed to open Windows Terminal", ex, GetType()); + //_context.API.ShowMsg(name, message, string.Empty); + } + } +#pragma warning restore IDE0059, CS0168, SA1005 + + private void Launch(string id, string profile) + { + var appManager = new ApplicationActivationManager(); + const ActivateOptions noFlags = ActivateOptions.None; + var queryArguments = TerminalHelper.GetArguments(profile, _openNewTab, _openQuake); + try + { + appManager.ActivateApplication(id, queryArguments, noFlags, out var unusedPid); + } +#pragma warning disable IDE0059, CS0168 + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + // var name = "Plugin: " + Resources.plugin_name; + // var message = Resources.run_terminal_failed; + // Log.Exception("Failed to open Windows Terminal", ex, GetType()); + // _context.API.ShowMsg(name, message, string.Empty); + } + } +#pragma warning restore IDE0059, CS0168 + + public override CommandResult Invoke() + { + try + { + LaunchElevated(_id, _profile); + } + catch + { + // TODO GH #108 We need to figure out some logging + } + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs new file mode 100644 index 0000000000..25124fb33c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Commands; + +internal sealed partial class LaunchProfileCommand : InvokableCommand +{ + private readonly string _id; + private readonly string _profile; + private readonly bool _openNewTab; + private readonly bool _openQuake; + + internal LaunchProfileCommand(string id, string profile, string iconPath, bool openNewTab, bool openQuake) + { + this._id = id; + this._profile = profile; + this._openNewTab = openNewTab; + this._openQuake = openQuake; + + this.Name = Resources.launch_profile; + this.Icon = new IconInfo(iconPath); + } + + private void Launch(string id, string profile) + { + var appManager = new ApplicationActivationManager(); + const ActivateOptions noFlags = ActivateOptions.None; + var queryArguments = TerminalHelper.GetArguments(profile, _openNewTab, _openQuake); + try + { + appManager.ActivateApplication(id, queryArguments, noFlags, out var unusedPid); + } +#pragma warning disable IDE0059, CS0168 + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + // var name = "Plugin: " + Resources.plugin_name; + // var message = Resources.run_terminal_failed; + // Log.Exception("Failed to open Windows Terminal", ex, GetType()); + // _context.API.ShowMsg(name, message, string.Empty); + } + } +#pragma warning restore IDE0059, CS0168 + + public override CommandResult Invoke() + { + try + { + Launch(_id, _profile); + } + catch + { + // TODO GH #108 We need to figure out some logging + } + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ApplicationActivationManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ApplicationActivationManager.cs new file mode 100644 index 0000000000..a074d9c86d --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ApplicationActivationManager.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +// Application Activation Manager Class +[ComImport] +[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] +public class ApplicationActivationManager : IApplicationActivationManager +{ + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/] + public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/IApplicationActivationManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/IApplicationActivationManager.cs new file mode 100644 index 0000000000..e332eee6fd --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/IApplicationActivationManager.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs +[Flags] +public enum ActivateOptions +{ + None = 0x00000000, + DesignMode = 0x00000001, + NoErrorUI = 0x00000002, + NoSplashScreen = 0x00000004, +} + +// ApplicationActivationManager +[ComImport] +[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IApplicationActivationManager +{ + IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ITerminalQuery.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ITerminalQuery.cs new file mode 100644 index 0000000000..339e0d735c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ITerminalQuery.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public interface ITerminalQuery +{ + IEnumerable GetProfiles(); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs new file mode 100644 index 0000000000..d1c8be21f4 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +#nullable enable + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "wt"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private readonly ToggleSetting _showHiddenProfiles = new( + Namespaced(nameof(ShowHiddenProfiles)), + Resources.show_hidden_profiles, + Resources.show_hidden_profiles, + false); + + private readonly ToggleSetting _openNewTab = new( + Namespaced(nameof(OpenNewTab)), + Resources.open_new_tab, + Resources.open_new_tab, + false); + + private readonly ToggleSetting _openQuake = new( + Namespaced(nameof(OpenQuake)), + Resources.open_quake, + Resources.open_quake_description, + false); + + public bool ShowHiddenProfiles => _showHiddenProfiles.Value; + + public bool OpenNewTab => _openNewTab.Value; + + public bool OpenQuake => _openQuake.Value; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_showHiddenProfiles); + Settings.Add(_openNewTab); + Settings.Add(_openQuake); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalHelper.cs new file mode 100644 index 0000000000..d5bb639e39 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalHelper.cs @@ -0,0 +1,102 @@ +// 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.Text.Json; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public static class TerminalHelper +{ + /// + /// Return the arguments for launch Windows Terminal + /// + /// Name of the Terminal profile + /// Whether to launch the profile in a new tab + /// Whether to launch the profile in the quake window + public static string GetArguments(string profileName, bool openNewTab, bool openQuake) + { + var argsPrefix = string.Empty; + if (openQuake) + { + // It does not matter whether we add the "nt" argument here; when specifying the + // _quake window explicitly, Windows Terminal will always open a new tab when the + // window exists, or open a new window when it does not yet. + argsPrefix = "--window _quake"; + } + else if (openNewTab) + { + argsPrefix = "--window 0 nt"; + } + + return $"{argsPrefix} --profile \"{profileName}\""; + } + + /// + /// Return a list of profiles for the Windows Terminal + /// + /// Windows Terminal package + /// Content of the settings JSON file of the Terminal + public static List ParseSettings(TerminalPackage terminal, string settingsJson) + { + var profiles = new List(); + + var options = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + var json = JsonDocument.Parse(settingsJson, options); + JsonElement profilesList; + + json.RootElement.TryGetProperty("profiles", out var profilesElement); + if (profilesElement.ValueKind == JsonValueKind.Object) + { + profilesElement.TryGetProperty("list", out profilesList); + if (profilesList.ValueKind != JsonValueKind.Array) + { + return profiles; + } + } + else if (profilesElement.ValueKind == JsonValueKind.Array) + { + profilesList = profilesElement; + } + else + { + return profiles; + } + + foreach (var profile in profilesList.EnumerateArray()) + { + profiles.Add(ParseProfile(terminal, profile)); + } + + return profiles; + } + + /// + /// Return a profile for the Windows Terminal + /// + /// Windows Terminal package + /// Profile from the settings JSON file + public static TerminalProfile ParseProfile(TerminalPackage terminal, JsonElement profileElement) + { + profileElement.TryGetProperty("name", out var nameElement); + var name = nameElement.ValueKind == JsonValueKind.String ? nameElement.GetString() : null; + + profileElement.TryGetProperty("hidden", out var hiddenElement); + var hidden = (hiddenElement.ValueKind == JsonValueKind.False || hiddenElement.ValueKind == JsonValueKind.True) && hiddenElement.GetBoolean(); + + profileElement.TryGetProperty("guid", out var guidElement); + var guid = guidElement.ValueKind == JsonValueKind.String ? Guid.Parse(guidElement.GetString()) : null as Guid?; + + profileElement.TryGetProperty("icon", out var iconElement); + var icon = iconElement.ValueKind == JsonValueKind.String ? iconElement.GetString() : null; + + return new TerminalProfile(terminal, name, guid, hidden, icon); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs new file mode 100644 index 0000000000..088c488ad3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Security.Principal; + +using Windows.Management.Deployment; + +// using Wox.Plugin.Logger; +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public class TerminalQuery : ITerminalQuery +{ + private readonly PackageManager _packageManager; + + // Static list of all Windows Terminal packages. + private static ReadOnlyCollection Packages => new List + { + "Microsoft.WindowsTerminal", + "Microsoft.WindowsTerminalPreview", + "Microsoft.WindowsTerminalCanary", + }.AsReadOnly(); + + private IEnumerable Terminals => GetTerminals(); + + public TerminalQuery() + { + _packageManager = new PackageManager(); + } + + public IEnumerable GetProfiles() + { + var profiles = new List(); + + if (!Terminals.Any()) + { + // TODO: what kind of logging should we do? + // Log.Warn($"No Windows Terminal packages installed", typeof(TerminalQuery)); + } + + foreach (var terminal in Terminals) + { + if (!File.Exists(terminal.SettingsPath)) + { + // TODO: what kind of logging should we do? + // Log.Warn($"Failed to find settings file {terminal.SettingsPath}", typeof(TerminalQuery)); + continue; + } + + var settingsJson = File.ReadAllText(terminal.SettingsPath); + profiles.AddRange(TerminalHelper.ParseSettings(terminal, settingsJson)); + } + + return profiles.OrderBy(p => p.Name); + } + + private IEnumerable GetTerminals() + { + var user = WindowsIdentity.GetCurrent().User; + var localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA"); + + foreach (var p in _packageManager.FindPackagesForUser(user.Value).Where(p => Packages.Contains(p.Id.Name))) + { + var appListEntries = p.GetAppListEntries(); + + var aumid = appListEntries.Single().AppUserModelId; + var version = new Version(p.Id.Version.Major, p.Id.Version.Minor, p.Id.Version.Build, p.Id.Version.Revision); + var settingsPath = Path.Combine(localAppDataPath, "Packages", p.Id.FamilyName, "LocalState", "settings.json"); + yield return new TerminalPackage(aumid, version, p.DisplayName, settingsPath, p.Logo.LocalPath); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj new file mode 100644 index 0000000000..7ea6f17148 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj @@ -0,0 +1,47 @@ + + + + + Microsoft.CmdPal.Ext.WindowsTerminal + + false + false + + Microsoft.CmdPal.Ext.WindowsTerminal.pri + + + + + + + + + + Resources.resx + True + True + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + Resources.Designer.cs + ResXFileCodeGenerator + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs new file mode 100644 index 0000000000..752aca5574 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.WindowsTerminal.Commands; +using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.UI.Xaml.Media.Imaging; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages; + +internal sealed partial class ProfilesListPage : ListPage +{ + private readonly TerminalQuery _terminalQuery = new(); + private readonly SettingsManager _terminalSettings; + private readonly Dictionary _logoCache = []; + + private bool showHiddenProfiles; + private bool openNewTab; + private bool openQuake; + + public ProfilesListPage(SettingsManager terminalSettings) + { + Icon = WindowsTerminalCommandsProvider.TerminalIcon; + Name = Resources.profiles_list_page_name; + _terminalSettings = terminalSettings; + } + +#pragma warning disable SA1108 + public List Query() + { + showHiddenProfiles = _terminalSettings.ShowHiddenProfiles; + openNewTab = _terminalSettings.OpenNewTab; + openQuake = _terminalSettings.OpenQuake; + + var profiles = _terminalQuery.GetProfiles(); + + var result = new List(); + + foreach (var profile in profiles) + { + if (profile.Hidden && !showHiddenProfiles) + { + continue; + } + + result.Add(new ListItem(new LaunchProfileCommand(profile.Terminal.AppUserModelId, profile.Name, profile.Terminal.LogoPath, openNewTab, openQuake)) + { + Title = profile.Name, + Subtitle = profile.Terminal.DisplayName, + MoreCommands = [ + new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)), + ], + + // Icon = () => GetLogo(profile.Terminal), + // Action = _ => + // { + // Launch(profile.Terminal.AppUserModelId, profile.Name); + // return true; + // }, + // ContextData = profile, +#pragma warning restore SA1108 + }); + } + + return result; + } + + public override IListItem[] GetItems() => Query().ToArray(); + + private BitmapImage GetLogo(TerminalPackage terminal) + { + var aumid = terminal.AppUserModelId; + + if (!_logoCache.TryGetValue(aumid, out var value)) + { + value = terminal.GetLogo(); + _logoCache.Add(aumid, value); + } + + return value; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..d6fa47029c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs @@ -0,0 +1,180 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.WindowsTerminal.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Windows Terminal Profiles. + /// + internal static string extension_name { + get { + return ResourceManager.GetString("extension_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch profile. + /// + internal static string launch_profile { + get { + return ResourceManager.GetString("launch_profile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch profile as administrator. + /// + internal static string launch_profile_as_admin { + get { + return ResourceManager.GetString("launch_profile_as_admin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Windows Terminal Profiles. + /// + internal static string list_item_title { + get { + return ResourceManager.GetString("list_item_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open profiles in a new tab. + /// + internal static string open_new_tab { + get { + return ResourceManager.GetString("open_new_tab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open profiles in the quake window. + /// + internal static string open_quake { + get { + return ResourceManager.GetString("open_quake", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Terminal supports a "quake" feature where a terminal window is accessible using a global hotkey. Enable this option to open profiles in a new tab in this window.. + /// + internal static string open_quake_description { + get { + return ResourceManager.GetString("open_quake_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opens Windows Terminal profiles. + /// + internal static string plugin_description { + get { + return ResourceManager.GetString("plugin_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Terminal Profiles. + /// + internal static string profiles_list_page_name { + get { + return ResourceManager.GetString("profiles_list_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run as administrator (Ctrl+Shift+Enter). + /// + internal static string run_as_administrator { + get { + return ResourceManager.GetString("run_as_administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to open Windows Terminal. + /// + internal static string run_terminal_failed { + get { + return ResourceManager.GetString("run_terminal_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + internal static string settings_page_name { + get { + return ResourceManager.GetString("settings_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show hidden profiles. + /// + internal static string show_hidden_profiles { + get { + return ResourceManager.GetString("show_hidden_profiles", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx new file mode 100644 index 0000000000..3b2ab8da6a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Open profiles in a new tab + + + Open profiles in the quake window + Quake is a well-known computer game. Don't localize. See https://en.wikipedia.org/wiki/Quake_(video_game) + + + Windows Terminal supports a "quake" feature where a terminal window is accessible using a global hotkey. Enable this option to open profiles in a new tab in this window. + Quake is a well-known computer game. Don't localize. See https://en.wikipedia.org/wiki/Quake_(video_game) + + + Opens Windows Terminal profiles + + + Windows Terminal Profiles + + + Run as administrator (Ctrl+Shift+Enter) + + + Failed to open Windows Terminal + + + Show hidden profiles + + + Launch profile + + + Launch profile as administrator + + + Windows Terminal Profiles + + + Settings + + + Open Windows Terminal Profiles + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs new file mode 100644 index 0000000000..08f95c04fa --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalPackage.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.UI.Xaml.Media.Imaging; + +// using Wox.Infrastructure.Image; +namespace Microsoft.CmdPal.Ext.WindowsTerminal; + +public class TerminalPackage +{ + public string AppUserModelId { get; } + + public Version Version { get; } + + public string DisplayName { get; } + + public string SettingsPath { get; } + + public string LogoPath { get; } + + public TerminalPackage(string appUserModelId, Version version, string displayName, string settingsPath, string logoPath) + { + AppUserModelId = appUserModelId; + Version = version; + DisplayName = displayName; + SettingsPath = settingsPath; + LogoPath = logoPath; + } + + public BitmapImage GetLogo() + { + var image = new BitmapImage(); + + if (File.Exists(LogoPath)) + { + using var fileStream = File.OpenRead(LogoPath); + image.SetSource(fileStream.AsRandomAccessStream()); + } + else + { + // Not using wox anymore, TODO: find the right new way to handle this + // image.UriSource = new Uri(ImageLoader.ErrorIconPath); + } + + return image; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalProfile.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalProfile.cs new file mode 100644 index 0000000000..2eae2fdeba --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalProfile.cs @@ -0,0 +1,29 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal; + +public class TerminalProfile +{ + public TerminalPackage Terminal { get; } + + public string Name { get; } + + public Guid? Identifier { get; } + + public bool Hidden { get; } + + public string Icon { get; } + + public TerminalProfile(TerminalPackage terminal, string name, Guid? identifier, bool hidden, string icon) + { + Terminal = terminal; + Name = name; + Identifier = identifier; + Hidden = hidden; + Icon = icon; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs new file mode 100644 index 0000000000..8f0f81c7c1 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs @@ -0,0 +1,20 @@ +// 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.WindowsTerminal.Helpers; +using Microsoft.CmdPal.Ext.WindowsTerminal.Pages; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal; + +public partial class TerminalTopLevelCommandItem : CommandItem +{ + public TerminalTopLevelCommandItem(SettingsManager settingsManager) + : base(new ProfilesListPage(settingsManager)) + { + Icon = WindowsTerminalCommandsProvider.TerminalIcon; + Title = Resources.list_item_title; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs new file mode 100644 index 0000000000..cc311541ef --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal; + +public partial class WindowsTerminalCommandsProvider : CommandProvider +{ + private readonly TerminalTopLevelCommandItem _terminalCommand; + private readonly SettingsManager _settingsManager = new(); + + public static IconInfo TerminalIcon { get; } = IconHelpers.FromRelativePath("Assets\\WindowsTerminal.svg"); + + public WindowsTerminalCommandsProvider() + { + Id = "WindowsTerminalProfiles"; + DisplayName = Resources.extension_name; + Icon = TerminalIcon; + Settings = _settingsManager.Settings; + + _terminalCommand = new TerminalTopLevelCommandItem(_settingsManager) + { + MoreCommands = [ + new CommandContextItem(Settings.SettingsPage), + ], + }; + } + + public override ICommandItem[] TopLevelCommands() => [_terminalCommand]; +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/LockScreenLogo.scale-200.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/SplashScreen.scale-200.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..32f486a867 Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..53ee3777ea Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..f713bba67f Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..dc9f5bea0c Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/StoreLogo.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/StoreLogo.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..8b4a5d0dd5 Binary files /dev/null and b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Package.appxmanifest b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Package.appxmanifest new file mode 100644 index 0000000000..a1caa748f7 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Package.appxmanifest @@ -0,0 +1,78 @@ + + + + + + + + Process Monitor Sample Extension + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessItem.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessItem.cs new file mode 100644 index 0000000000..2c9653d13f --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessItem.cs @@ -0,0 +1,22 @@ +// 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; + +namespace ProcessMonitorExtension; + +internal sealed class ProcessItem +{ + internal Process Process { get; init; } + + internal int ProcessId { get; init; } + + internal string Name { get; init; } + + internal string ExePath { get; init; } + + internal long Memory { get; init; } + + internal long CPU { get; init; } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessListPage.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessListPage.cs new file mode 100644 index 0000000000..dc218f6230 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessListPage.cs @@ -0,0 +1,71 @@ +// 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.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace ProcessMonitorExtension; + +internal sealed partial class ProcessListPage : ListPage +{ + public ProcessListPage() + { + this.Icon = new IconInfo("\ue9d9"); + this.Name = "Process Monitor"; + } + + public override IListItem[] GetItems() => DoGetItems(); + + internal void UpdateItems() => this.RaiseItemsChanged(-1); + + private IListItem[] DoGetItems() + { + var items = GetRunningProcesses(); + this.IsLoading = false; + var s = items + .OrderByDescending(p => p.Memory) + .Select((process) => new ListItem(new SwitchToProcess(process)) + { + Title = process.Name, + Subtitle = $"PID: {process.ProcessId}", + MoreCommands = [ + new CommandContextItem(new TerminateProcess(process, this)) + ], + }).ToArray(); + return s; + } + + private static IEnumerable GetRunningProcesses() + { + return Process.GetProcesses() + .Select(p => + { + var exePath = string.Empty; + + try + { + exePath = p.MainModule.FileName; + } + catch + { + // Handle cases where the icon extraction or file path retrieval fails + } + + return new ProcessItem + { + Process = p, + ProcessId = p.Id, + Name = p.ProcessName, + ExePath = exePath, + Memory = p.WorkingSet64, + + // oh no CPU is not trivial to get + }; + }); + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorCommandProvider.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorCommandProvider.cs new file mode 100644 index 0000000000..0370398955 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorCommandProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace ProcessMonitorExtension; + +internal sealed partial class ProcessMonitorCommandProvider : CommandProvider +{ + public ProcessMonitorCommandProvider() + { + DisplayName = "Process Monitor Commands"; + } + + private readonly ICommandItem[] _commands = [ + new CommandItem(new ProcessListPage()) + { + Title = "Process Manager", + Subtitle = "Kill processes", + }, + ]; + + public override ICommandItem[] TopLevelCommands() + { + return _commands; + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorExtension.csproj b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorExtension.csproj new file mode 100644 index 0000000000..d36c277705 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/ProcessMonitorExtension.csproj @@ -0,0 +1,51 @@ + + + + WinExe + ProcessMonitorExtension + app.manifest + win-$(Platform).pubxml + false + true + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace) + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Program.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Program.cs new file mode 100644 index 0000000000..126b7ee8c3 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace ProcessMonitorExtension; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + using ExtensionServer server = new(); + var extensionDisposedEvent = new ManualResetEvent(false); + var extensionInstance = new SampleExtension(extensionDisposedEvent); + + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + server.RegisterExtension(() => extensionInstance); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as soon as it is disposed. + extensionDisposedEvent.WaitOne(); + } + else + { + Console.WriteLine("Not being launched as a Extension... exiting."); + } + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..cea430ad55 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,18 @@ + + + + + FileSystem + ARM64 + win-arm64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + False + False + + diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..2c790532c6 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,18 @@ + + + + + FileSystem + x64 + win-x64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + False + False + + diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/launchSettings.json b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..f9566c1552 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ProcessMonitorExtension (Package)": { + "commandName": "MsixPackage", + "doNotLaunchApp": true + }, + "ProcessMonitorExtension (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs new file mode 100644 index 0000000000..f7fdc2cb27 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SampleExtension.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace ProcessMonitorExtension; + +[ComVisible(true)] +[Guid("8BD7A6C4-7185-4426-AE8D-61E438A3E740")] +[ComDefaultInterface(typeof(IExtension))] +public sealed partial class SampleExtension : IExtension, IDisposable +{ + private readonly ManualResetEvent _extensionDisposedEvent; + + private readonly ProcessMonitorCommandProvider _provider = new(); + + public SampleExtension(ManualResetEvent extensionDisposedEvent) + { + this._extensionDisposedEvent = extensionDisposedEvent; + } + + public object GetProvider(ProviderType providerType) + { + switch (providerType) + { + case ProviderType.Commands: + return _provider; + default: + return null; + } + } + + public void Dispose() + { + this._extensionDisposedEvent.Set(); + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/SwitchToProcess.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SwitchToProcess.cs new file mode 100644 index 0000000000..28ed75598a --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/SwitchToProcess.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace ProcessMonitorExtension; + +internal sealed partial class SwitchToProcess : InvokableCommand +{ + [DllImport("user32.dll")] + public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab); + + private readonly ProcessItem process; + + public SwitchToProcess(ProcessItem process) + { + this.process = process; + this.Icon = new IconInfo(process.ExePath == string.Empty ? "\uE7B8" : process.ExePath); + this.Name = "Switch to"; + } + + public override CommandResult Invoke() + { + SwitchToThisWindow(process.Process.MainWindowHandle, true); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/TerminateProcess.cs b/src/modules/cmdpal/Exts/ProcessMonitorExtension/TerminateProcess.cs new file mode 100644 index 0000000000..0dd9914c59 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/TerminateProcess.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace ProcessMonitorExtension; + +internal sealed partial class TerminateProcess : InvokableCommand +{ + private readonly ProcessItem _process; + private readonly ProcessListPage _owner; + + public TerminateProcess(ProcessItem process, ProcessListPage owner) + { + _process = process; + _owner = owner; + Icon = new IconInfo("\ue74d"); + Name = "End task"; + } + + public override CommandResult Invoke() + { + var process = Process.GetProcessById(_process.ProcessId); + process.Kill(); + _owner.UpdateItems(); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/ProcessMonitorExtension/app.manifest b/src/modules/cmdpal/Exts/ProcessMonitorExtension/app.manifest new file mode 100644 index 0000000000..a620209321 --- /dev/null +++ b/src/modules/cmdpal/Exts/ProcessMonitorExtension/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/LockScreenLogo.scale-200.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/SplashScreen.scale-200.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..32f486a867 Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..53ee3777ea Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..f713bba67f Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..dc9f5bea0c Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/StoreLogo.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/StoreLogo.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..8b4a5d0dd5 Binary files /dev/null and b/src/modules/cmdpal/Exts/SamplePagesExtension/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSampleListPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSampleListPage.cs new file mode 100644 index 0000000000..3be5882aa3 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSampleListPage.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class EvilSampleListPage : ListPage +{ + public EvilSampleListPage() + { + Icon = new IconInfo(string.Empty); + Name = "Open"; + Title = "Evil Sample Page"; + } + + public override IListItem[] GetItems() + { + IListItem[] commands = [ + new ListItem(new EvilSampleListPage()) + { + Subtitle = "Doesn't matter, I'll blow up before you see this", + }, + ]; + + _ = commands[9001]; // Throws + + return commands; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSamplesPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSamplesPage.cs new file mode 100644 index 0000000000..373a1f7891 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/EvilSamplesPage.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +public partial class EvilSamplesPage : ListPage +{ + private readonly IListItem[] _commands = [ + new ListItem(new EvilSampleListPage()) + { + Title = "List Page without items", + Subtitle = "Throws exception on GetItems", + }, + new ListItem(new ExplodeInFiveSeconds(false)) + { + Title = "Page that will throw an exception after loading it", + Subtitle = "Throws exception on GetItems _after_ a ItemsChanged", + }, + new ListItem(new ExplodeInFiveSeconds(true)) + { + Title = "Page that keeps throwing exceptions", + Subtitle = "Will throw every 5 seconds once you open it", + }, + new ListItem(new ExplodeOnPropChange()) + { + Title = "Throw in the middle of a PropChanged", + Subtitle = "Will throw every 5 seconds once you open it", + }, + new ListItem(new SelfImmolateCommand()) + { + Title = "Terminate this extension", + Subtitle = "Will exit this extension (while it's loaded!)", + }, + new ListItem(new NoOpCommand()) + { + Title = "I have lots of nulls", + Subtitle = null, + MoreCommands = null, + Tags = null, + Details = new Details() + { + Title = null, + HeroImage = null, + Metadata = null, + }, + }, + new ListItem(new NoOpCommand()) + { + Title = "I also have nulls", + Subtitle = null, + MoreCommands = null, + Details = new Details() + { + Title = null, + HeroImage = null, + Metadata = [new DetailsElement() { Key = "Oops all nulls", Data = new DetailsTags() { Tags = null } }], + }, + }, + new ListItem(new AnonymousCommand(action: () => + { + ToastStatusMessage toast = new("I should appear immediately"); + toast.Show(); + Thread.Sleep(5000); + }) { Result = CommandResult.KeepOpen() }) + { + Title = "I take just forever to return something", + Subtitle = "The toast should appear immediately.", + MoreCommands = null, + Details = new Details() + { + Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.", + }, + } + ]; + + public EvilSamplesPage() + { + Name = "Evil Samples"; + Icon = new IconInfo("👿"); // Info + } + + public override IListItem[] GetItems() => _commands; +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] +internal sealed partial class ExplodeOnPropChange : ListPage +{ + private bool _explode; + + public override string Title + { + get => _explode ? Commands[9001].Title : base.Title; + set => base.Title = value; + } + + private IListItem[] Commands => [ + new ListItem(new NoOpCommand()) + { + Title = "This page will explode in five seconds!", + Subtitle = "I'll change my Name, then explode", + }, + ]; + + public ExplodeOnPropChange() + { + Icon = new IconInfo(string.Empty); + Name = "Open"; + } + + public override IListItem[] GetItems() + { + _ = Task.Run(() => + { + Thread.Sleep(1000); + Title = "Ready? 3..."; + Thread.Sleep(1000); + Title = "Ready? 2..."; + Thread.Sleep(1000); + Title = "Ready? 1..."; + Thread.Sleep(1000); + _explode = true; + Title = "boom"; + }); + return Commands; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/ExplodeInFiveSeconds.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/ExplodeInFiveSeconds.cs new file mode 100644 index 0000000000..41301099cf --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/ExplodeInFiveSeconds.cs @@ -0,0 +1,50 @@ +// 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.Timers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class ExplodeInFiveSeconds : ListPage +{ + private readonly bool _repeat; + + private IListItem[] Commands => [ + new ListItem(new NoOpCommand()) + { + Title = "This page will explode in five seconds!", + Subtitle = _repeat ? "Not only that, I'll _keep_ exploding every 5 seconds after that" : string.Empty, + }, + ]; + + private bool shouldExplode; + private static Timer timer; + + public ExplodeInFiveSeconds(bool repeat) + { + _repeat = repeat; + Icon = new IconInfo(string.Empty); + Name = "Open"; + } + + public override IListItem[] GetItems() + { + if (shouldExplode) + { + _ = Commands[9001]; // Throws + } + else + { + timer = new Timer(5000); + timer.Elapsed += (object source, ElapsedEventArgs e) => { RaiseItemsChanged(9000); }; + timer.AutoReset = _repeat; // Keep repeating + timer.Enabled = true; + } + + shouldExplode = true; + return Commands; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Package.appxmanifest b/src/modules/cmdpal/Exts/SamplePagesExtension/Package.appxmanifest new file mode 100644 index 0000000000..6b8fe2ac10 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Package.appxmanifest @@ -0,0 +1,78 @@ + + + + + + + + Sample Pages Extension + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs new file mode 100644 index 0000000000..a602f74a00 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleContentPage.cs @@ -0,0 +1,354 @@ +// 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.Text.Json.Nodes; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleContentPage : ContentPage +{ + private readonly SampleContentForm sampleForm = new(); + private readonly MarkdownContent sampleMarkdown = new() { Body = "# Sample page with mixed content \n This page has both markdown, and form content" }; + + public override IContent[] GetContent() => [sampleMarkdown, sampleForm]; + + public SampleContentPage() + { + Name = "Open"; + Title = "Sample Content"; + Icon = new IconInfo("\uECA5"); // Tiles + + Commands = [ + new CommandContextItem( + title: "Do thing", + name: "Do thing", + subtitle: "Pops a toast", + result: CommandResult.ShowToast(new ToastArgs() { Message = "what's up doc", Result = CommandResult.KeepOpen() }), + action: () => { Title = Title + "+1"; }), + new CommandContextItem( + title: "Something else", + name: "Something else", + subtitle: "Something else", + result: CommandResult.ShowToast(new ToastArgs() { Message = "turn down for what?", Result = CommandResult.KeepOpen() }), + action: () => { Title = Title + "-1"; }), + ]; + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] +internal sealed partial class SampleContentForm : FormContent +{ + public SampleContentForm() + { + TemplateJson = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.6", + "body": [ + { + "type": "TextBlock", + "size": "medium", + "weight": "bolder", + "text": " ${ParticipantInfoForm.title}", + "horizontalAlignment": "center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.Text", + "label": "Name", + "style": "text", + "id": "SimpleVal", + "isRequired": true, + "errorMessage": "Name is required", + "placeholder": "Enter your name" + }, + { + "type": "Input.Text", + "label": "Homepage", + "style": "url", + "id": "UrlVal", + "placeholder": "Enter your homepage url" + }, + { + "type": "Input.Text", + "label": "Email", + "style": "email", + "id": "EmailVal", + "placeholder": "Enter your email" + }, + { + "type": "Input.Text", + "label": "Phone", + "style": "tel", + "id": "TelVal", + "placeholder": "Enter your phone number" + }, + { + "type": "Input.Text", + "label": "Comments", + "style": "text", + "isMultiline": true, + "id": "MultiLineVal", + "placeholder": "Enter any comments" + }, + { + "type": "Input.Number", + "label": "Quantity (Minimum -5, Maximum 5)", + "min": -5, + "max": 5, + "value": 1, + "id": "NumVal", + "errorMessage": "The quantity must be between -5 and 5" + }, + { + "type": "Input.Date", + "label": "Due Date", + "id": "DateVal", + "value": "2017-09-20" + }, + { + "type": "Input.Time", + "label": "Start time", + "id": "TimeVal", + "value": "16:59" + }, + { + "type": "TextBlock", + "size": "medium", + "weight": "bolder", + "text": "${Survey.title} ", + "horizontalAlignment": "center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.ChoiceSet", + "id": "CompactSelectVal", + "label": "${Survey.questions[0].question}", + "style": "compact", + "value": "1", + "choices": [ + { + "$data": "${Survey.questions[0].items}", + "title": "${choice}", + "value": "${value}" + } + ] + }, + { + "type": "Input.ChoiceSet", + "id": "SingleSelectVal", + "label": "${Survey.questions[1].question}", + "style": "expanded", + "value": "1", + "choices": [ + { + "$data": "${Survey.questions[1].items}", + "title": "${choice}", + "value": "${value}" + } + ] + }, + { + "type": "Input.ChoiceSet", + "id": "MultiSelectVal", + "label": "${Survey.questions[2].question}", + "isMultiSelect": true, + "value": "1,3", + "choices": [ + { + "$data": "${Survey.questions[2].items}", + "title": "${choice}", + "value": "${value}" + } + ] + }, + { + "type": "TextBlock", + "size": "medium", + "weight": "bolder", + "text": "Input.Toggle", + "horizontalAlignment": "center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.Toggle", + "label": "Please accept the terms and conditions:", + "title": "${Survey.questions[3].question}", + "valueOn": "true", + "valueOff": "false", + "id": "AcceptsTerms", + "isRequired": true, + "errorMessage": "Accepting the terms and conditions is required" + }, + { + "type": "Input.Toggle", + "label": "How do you feel about red cars?", + "title": "${Survey.questions[4].question}", + "valueOn": "RedCars", + "valueOff": "NotRedCars", + "id": "ColorPreference" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Submit", + "data": { + "id": "1234567890" + } + }, + { + "type": "Action.ShowCard", + "title": "Show Card", + "card": { + "type": "AdaptiveCard", + "body": [ + { + "type": "Input.Text", + "label": "Enter comment", + "style": "text", + "id": "CommentVal" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "OK" + } + ] + } + } + ] +} +"""; + + DataJson = $$""" +{ + "ParticipantInfoForm": { + "title": "Input.Text elements" + }, + "Survey": { + "title": "Input ChoiceSet", + "questions": [ + { + "question": "What color do you want? (compact)", + "items": [ + { + "choice": "Red", + "value": "1" + }, + { + "choice": "Green", + "value": "2" + }, + { + "choice": "Blue", + "value": "3" + } + ] + }, + { + "question": "What color do you want? (expanded)", + "items": [ + { + "choice": "Red", + "value": "1" + }, + { + "choice": "Green", + "value": "2" + }, + { + "choice": "Blue", + "value": "3" + } + ] + }, + { + "question": "What color do you want? (multiselect)", + "items": [ + { + "choice": "Red", + "value": "1" + }, + { + "choice": "Green", + "value": "2" + }, + { + "choice": "Blue", + "value": "3" + } + ] + }, + { + "question": "I accept the terms and conditions (True/False)" + }, + { + "question": "Red cars are better than other cars" + } + ] + } +} +"""; + } + + public override CommandResult SubmitForm(string payload) + { + var formInput = JsonNode.Parse(payload)?.AsObject(); + if (formInput == null) + { + return CommandResult.GoHome(); + } + + // Application.Current.GetService().SaveSettingAsync("GlobalHotkey", formInput["hotkey"]?.ToString() ?? string.Empty); + return CommandResult.GoHome(); + } +} + +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] +internal sealed partial class SampleTreeContentPage : ContentPage +{ + private readonly TreeContent myContentTree; + + public override IContent[] GetContent() => [myContentTree]; + + public SampleTreeContentPage() + { + Name = Title = "Sample Content"; + Icon = new IconInfo("\uE81E"); + + myContentTree = new() + { + RootContent = new MarkdownContent() { Body = "# This page has nested content" }, + Children = [ + new TreeContent() + { + RootContent = new MarkdownContent() { Body = "Yo dog" }, + Children = [ + new TreeContent() + { + RootContent = new MarkdownContent() { Body = "I heard you like content" }, + Children = [ + new MarkdownContent() { Body = "So we put content in your content" }, + new FormContent() { TemplateJson = "{\"$schema\":\"http://adaptivecards.io/schemas/adaptive-card.json\",\"type\":\"AdaptiveCard\",\"version\":\"1.6\",\"body\":[{\"type\":\"TextBlock\",\"size\":\"medium\",\"weight\":\"bolder\",\"text\":\"Mix and match why don't you\",\"horizontalAlignment\":\"center\",\"wrap\":true,\"style\":\"heading\"},{\"type\":\"TextBlock\",\"text\":\"You can have forms here too\",\"horizontalAlignment\":\"Right\",\"wrap\":true}],\"actions\":[{\"type\":\"Action.Submit\",\"title\":\"It's a form, you get it\",\"data\":{\"id\":\"LoginVal\"}}]}" }, + new MarkdownContent() { Body = "Another markdown down here" }, + ], + }, + new MarkdownContent() { Body = "**slaps roof**" }, + new MarkdownContent() { Body = "This baby can fit so much content" }, + + ], + } + ], + }; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleDynamicListPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleDynamicListPage.cs new file mode 100644 index 0000000000..c284c7d784 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleDynamicListPage.cs @@ -0,0 +1,38 @@ +// 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.Runtime.InteropServices.WindowsRuntime; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleDynamicListPage : DynamicListPage +{ + public SampleDynamicListPage() + { + Icon = new IconInfo(string.Empty); + Name = "Dynamic List"; + IsLoading = true; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(newSearch.Length); + + public override IListItem[] GetItems() + { + var items = SearchText.ToCharArray().Select(ch => new ListItem(new NoOpCommand()) { Title = ch.ToString() }).ToArray(); + if (items.Length == 0) + { + items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }]; + } + + if (items.Length > 0) + { + items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box"; + } + + return items; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs new file mode 100644 index 0000000000..da1e1f9cdb --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPage.cs @@ -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 Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleListPage : ListPage +{ + public SampleListPage() + { + Icon = new IconInfo("\uEA37"); + Name = "Sample List Page"; + } + + public override IListItem[] GetItems() + { + var confirmOnceArgs = new ConfirmationArgs + { + PrimaryCommand = new AnonymousCommand( + () => + { + var t = new ToastStatusMessage("The dialog was confirmed"); + t.Show(); + }) + { + Name = "Confirm", + Result = CommandResult.KeepOpen(), + }, + Title = "You can set a title for the dialog", + Description = "Are you really sure you want to do the thing?", + }; + var confirmTwiceArgs = new ConfirmationArgs + { + PrimaryCommand = new AnonymousCommand(() => { }) + { + Name = "How sure are you?", + Result = CommandResult.Confirm(confirmOnceArgs), + }, + Title = "You can ask twice too", + Description = "You probably don't want to though, that'd be annoying.", + }; + + return [ + new ListItem(new NoOpCommand()) + { + Title = "This is a basic item in the list", + Subtitle = "I don't do anything though", + }, + new ListItem(new SampleListPageWithDetails()) + { + Title = "This item will take you to another page", + Subtitle = "This allows for nested lists of items", + }, + new ListItem(new SampleMarkdownPage()) + { + Title = "Items can have tags", + Subtitle = "and I'll take you to a page with markdown content", + Tags = [new Tag("Sample Tag")], + }, + new ListItem(new SendMessageCommand()) + { + Title = "I send lots of messages", + Subtitle = "Status messages can be used to provide feedback to the user in-app", + }, + new SendSingleMessageItem(), + new ListItem(new IndeterminateProgressMessageCommand()) + { + Title = "Do a thing with a spinner", + Subtitle = "Messages can have progress spinners, to indicate something is happening in the background", + }, + new ListItem( + new AnonymousCommand(() => { }) + { + Result = CommandResult.Confirm(confirmOnceArgs), + }) + { + Title = "Confirm before doing something", + }, + new ListItem( + new AnonymousCommand(() => { }) + { + Result = CommandResult.Confirm(confirmTwiceArgs), + }) + { + Title = "Confirm twice before doing something", + } + ]; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPageWithDetails.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPageWithDetails.cs new file mode 100644 index 0000000000..9495a7f6bf --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleListPageWithDetails.cs @@ -0,0 +1,145 @@ +// 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 SamplePagesExtension; + +internal sealed partial class SampleListPageWithDetails : ListPage +{ + public SampleListPageWithDetails() + { + Icon = new IconInfo("\uE8A0"); + Name = Title = "Sample List Page with Details"; + this.ShowDetails = true; + } + + public override IListItem[] GetItems() + { + return [ + new ListItem(new NoOpCommand()) + { + Title = "This page demonstrates Details on ListItems", + Details = new Details() + { + Title = "List Item 1", + Body = "Each of these items can have a `Body` formatted with **Markdown**", + }, + }, + new ListItem(new NoOpCommand()) + { + Title = "This one has a subtitle too", + Subtitle = "Example Subtitle", + Details = new Details() + { + Title = "List Item 2", + Body = SampleMarkdownPage.SampleMarkdownText, + }, + }, + new ListItem(new NoOpCommand()) + { + Title = "This one has a tag too", + Subtitle = "the one with a tag", + Tags = [ + new Tag() + { + Text = "Sample Tag", + } + ], + Details = new Details() + { + Title = "List Item 3", + Body = "### Example of markdown details", + }, + }, + new ListItem(new NoOpCommand()) + { + Title = "This one has a hero image", + Tags = [], + Details = new Details() + { + Title = "Hero Image Example", + HeroImage = new IconInfo("https://m.media-amazon.com/images/M/MV5BNDBkMzVmNGQtYTM2OC00OWRjLTk5OWMtNzNkMDI4NjFjNTZmXkEyXkFqcGdeQXZ3ZXNsZXk@._V1_QL75_UX500_CR0,0,500,281_.jpg"), + Body = "It is literally an image of a hero", + }, + }, + new ListItem(new NoOpCommand()) + { + Title = "This one has metadata", + Tags = [], + Details = new Details() + { + Title = "Metadata Example", + Body = "Each of the sections below is some sample metadata", + Metadata = [ + new DetailsElement() + { + Key = "Plain text", + Data = new DetailsLink() { Text = "Set just the text to get text metadata" }, + }, + new DetailsElement() + { + Key = "Links", + Data = new DetailsLink() { Text = "Or metadata can be links", Link = new("https://github.com/microsoft/PowerToys") }, + }, + new DetailsElement() + { + Key = "CmdPal will display the URL if no text is given", + Data = new DetailsLink() { Link = new("https://github.com/microsoft/PowerToys") }, + }, + new DetailsElement() + { + Key = "Above a separator", + Data = new DetailsLink() { Text = "Below me is a separator" }, + }, + new DetailsElement() + { + Key = "A separator", + Data = new DetailsSeparator(), + }, + new DetailsElement() + { + Key = "Below a separator", + Data = new DetailsLink() { Text = "Above me is a separator" }, + }, + new DetailsElement() + { + Key = "Add Tags too", + Data = new DetailsTags() + { + Tags = [ + new Tag("simple text"), + new Tag("Colored text") { Foreground = ColorHelpers.FromRgb(255, 0, 0) }, + new Tag("Colored backgrounds") { Background = ColorHelpers.FromRgb(0, 0, 255) }, + new Tag("Colored everything") { Foreground = ColorHelpers.FromRgb(255, 255, 0), Background = ColorHelpers.FromRgb(0, 0, 255) }, + new Tag("Icons too") { Icon = new IconInfo("\uE735"), Foreground = ColorHelpers.FromRgb(255, 255, 0) }, + new Tag() { Icon = new IconInfo("https://i.imgur.com/t9qgDTM.png") }, + new Tag("this") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("baby") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("can") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("fit") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("so") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("many") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("tags") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("in") { Foreground = RandomColor(), Background = RandomColor() }, + new Tag("it") { Foreground = RandomColor(), Background = RandomColor() }, + ], + }, + }, + ], + }, + } + ]; + } + + private static OptionalColor RandomColor() + { + var r = new Random(); + var b = new byte[3]; + r.NextBytes(b); + return ColorHelpers.FromRgb(b[0], b[1], b[2]); + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleMarkdownPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleMarkdownPage.cs new file mode 100644 index 0000000000..3bfa786fb3 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleMarkdownPage.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleMarkdownPage : ContentPage +{ + public static readonly string SampleMarkdownText = @" +# Markdown Guide + +Markdown is a lightweight markup language with plain text formatting syntax. It's often used to format readme files, for writing messages in online forums, and to create rich text using a simple, plain text editor. + +## Basic Markdown Formatting + +### Headings + + # This is an

tag + ## This is an

tag + ### This is an

tag + #### This is an

tag + ##### This is an

tag + ###### This is an
tag + +### Emphasis + + *This text will be italic* + _This will also be italic_ + + **This text will be bold** + __This will also be bold__ + + _You **can** combine them_ + +Result: + +*This text will be italic* + +_This will also be italic_ + +**This text will be bold** + +__This will also be bold__ + +_You **can** combine them_ + +### Lists + +**Inordered:** + + * Milk + * Bread + * Wholegrain + * Butter + +Result: + +* Milk +* Bread + * Wholegrain +* Butter + +**Ordered:** + + 1. Tidy the kitchen + 2. Prepare ingredients + 3. Cook delicious things + +Result: + +1. Tidy the kitchen +2. Prepare ingredients +3. Cook delicious things + +### Images + + ![Alt Text](url) + +Result: + +![painting](https://i.imgur.com/93XJSNh.png) + +### Links + + [example](http://example.com) + +Result: + +[example](http://example.com) + +### Blockquotes + + As Albert Einstein said: + + > If we knew what it was we were doing, + > it would not be called research, would it? + +Result: + +As Albert Einstein said: + +> If we knew what it was we were doing, +> it would not be called research, would it? + +### Horizontal Rules + +```markdown + --- +``` + +Result: + +--- + +### Code Snippets + + Indenting by 4 spaces will turn an entire paragraph into a code-block. + +Result: + + .my-link { + text-decoration: underline; + } + +### Reference Lists & Titles + + **The quick brown [fox][1], jumped over the lazy [dog][2].** + + [1]: https://en.wikipedia.org/wiki/Fox ""Wikipedia: Fox"" + [2]: https://en.wikipedia.org/wiki/Dog ""Wikipedia: Dog"" + +Result: + +**The quick brown [fox][1], jumped over the lazy [dog][2].** + +[1]: https://en.wikipedia.org/wiki/Fox ""Wikipedia: Fox"" +[2]: https://en.wikipedia.org/wiki/Dog ""Wikipedia: Dog"" + +### Escaping + + \*literally\* + +Result: + +\*literally\* + +## Advanced Markdown + +Note: Some syntax which is not standard to native Markdown. They're extensions of the language. + +### Strike-throughs + + ~~deleted words~~ + +Result: + +~~deleted words~~ + + +"; + + public SampleMarkdownPage() + { + Icon = new IconInfo(string.Empty); + Name = "Sample Markdown Page"; + } + + public override IContent[] GetContent() => [new MarkdownContent(SampleMarkdownText)]; +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs new file mode 100644 index 0000000000..c0b88c567c --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Pages/SampleSettingsPage.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleSettingsPage : ContentPage +{ + private readonly Settings _settings = new(); + + private readonly List _choices = new() + { + new ChoiceSetSetting.Choice("The first choice in the list is the default choice", "0"), + new ChoiceSetSetting.Choice("Choices have titles and values", "1"), + new ChoiceSetSetting.Choice("Title", "Value"), + new ChoiceSetSetting.Choice("The options are endless", "3"), + new ChoiceSetSetting.Choice("So many choices", "4"), + }; + + public override IContent[] GetContent() + { + var s = _settings.ToContent(); + return s; + } + + public SampleSettingsPage() + { + Name = "Sample Settings"; + Icon = new IconInfo(string.Empty); + _settings.Add(new ToggleSetting("onOff", true) + { + Label = "This is a toggle", + Description = "It produces a simple checkbox", + }); + _settings.Add(new TextSetting("someText", "initial value") + { + Label = "This is a text box", + Description = "For some string of text", + }); + _settings.Add(new ChoiceSetSetting("choiceSetExample", _choices) + { + Label = "It also has a label", + Description = "Describe your choice set setting here", + }); + + _settings.SettingsChanged += SettingsChanged; + } + + private void SettingsChanged(object sender, Settings args) + { + /* Do something with the new settings here */ + var onOff = _settings.GetSetting("onOff"); + ExtensionHost.LogMessage(new LogMessage() { Message = $"SampleSettingsPage: Changed the value of onOff to {onOff}" }); + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Program.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/Program.cs new file mode 100644 index 0000000000..24efd1a1c6 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace SamplePagesExtension; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + using ExtensionServer server = new(); + var extensionDisposedEvent = new ManualResetEvent(false); + var extensionInstance = new SampleExtension(extensionDisposedEvent); + + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + server.RegisterExtension(() => extensionInstance); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as soon as it is disposed. + extensionDisposedEvent.WaitOne(); + } + else + { + Console.WriteLine("Not being launched as a Extension... exiting."); + } + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json b/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..b62e4e316c --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "SamplePagesExtension (Package)": { + "commandName": "MsixPackage", + "doNotLaunchApp": true, + "nativeDebugging": true + }, + "SamplePagesExtension (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs new file mode 100644 index 0000000000..cf8574ec22 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleExtension.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.CommandPalette.Extensions; + +namespace SamplePagesExtension; + +[ComVisible(true)] +[Guid("6112D28D-6341-45C8-92C3-83ED55853A9F")] +[ComDefaultInterface(typeof(IExtension))] +public sealed partial class SampleExtension : IExtension, IDisposable +{ + private readonly ManualResetEvent _extensionDisposedEvent; + + private readonly SamplePagesCommandsProvider _provider = new(); + + public SampleExtension(ManualResetEvent extensionDisposedEvent) + { + this._extensionDisposedEvent = extensionDisposedEvent; + } + + public object GetProvider(ProviderType providerType) + { + switch (providerType) + { + case ProviderType.Commands: + return _provider; + default: + return null; + } + } + + public void Dispose() + { + this._extensionDisposedEvent.Set(); + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs new file mode 100644 index 0000000000..e500c25d8d --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesCommandsProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +public partial class SamplePagesCommandsProvider : CommandProvider +{ + public SamplePagesCommandsProvider() + { + DisplayName = "Sample Pages Commands"; + Icon = new IconInfo("\uE82D"); + } + + private readonly ICommandItem[] _commands = [ + new CommandItem(new SamplesListPage()) + { + Title = "Sample Pages", + Subtitle = "View example commands", + }, + ]; + + public override ICommandItem[] TopLevelCommands() + { + return _commands; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj new file mode 100644 index 0000000000..c00df9b84e --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplePagesExtension.csproj @@ -0,0 +1,64 @@ + + + + + WinExe + SamplePagesExtension + app.manifest + win-$(Platform).pubxml + false + true + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace) + false + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + true + true + true + + + diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SampleUpdatingItemsPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleUpdatingItemsPage.cs new file mode 100644 index 0000000000..4b94a22ead --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SampleUpdatingItemsPage.cs @@ -0,0 +1,43 @@ +// 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.Timers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.VisualBasic; + +namespace SamplePagesExtension; + +public partial class SampleUpdatingItemsPage : ListPage +{ + private readonly ListItem hourItem = new(new NoOpCommand()); + private readonly ListItem minuteItem = new(new NoOpCommand()); + private readonly ListItem secondItem = new(new NoOpCommand()); + private static Timer timer; + + public SampleUpdatingItemsPage() + { + Name = "Open"; + Icon = new IconInfo("\uE72C"); + } + + public override IListItem[] GetItems() + { + if (timer == null) + { + timer = new Timer(500); + timer.Elapsed += (object source, ElapsedEventArgs e) => + { + var current = DateAndTime.Now; + hourItem.Title = $"{current.Hour}"; + minuteItem.Title = $"{current.Minute}"; + secondItem.Title = $"{current.Second}"; + }; + timer.AutoReset = true; // Keep repeating + timer.Enabled = true; + } + + return [hourItem, minuteItem, secondItem]; + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SamplesListPage.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplesListPage.cs new file mode 100644 index 0000000000..3eeed6c2c2 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SamplesListPage.cs @@ -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 Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +public partial class SamplesListPage : ListPage +{ + private readonly IListItem[] _commands = [ + + // List pages + new ListItem(new SampleListPage()) + { + Title = "List Page Sample Command", + Subtitle = "Display a list of items", + }, + new ListItem(new SampleListPageWithDetails()) + { + Title = "List Page With Details", + Subtitle = "A list of items, each with additional details to display", + }, + new ListItem(new SampleUpdatingItemsPage()) + { + Title = "List page with items that change", + Subtitle = "The items on the list update themselves in real time", + }, + new ListItem(new SampleDynamicListPage()) + { + Title = "Dynamic List Page Command", + Subtitle = "Changes the list of items in response to the typed query", + }, + + // Content pages + new ListItem(new SampleContentPage()) + { + Title = "Sample content page", + Subtitle = "Display mixed forms, markdown, and other types of content", + }, + new ListItem(new SampleTreeContentPage()) + { + Title = "Sample nested content", + Subtitle = "Example of nesting a tree of content", + }, + new ListItem(new SampleCommentsPage()) + { + Title = "Sample of nested comments", + Subtitle = "Demo of using nested trees of content to create a comment thread-like experience", + Icon = new IconInfo("\uE90A"), // Comment + }, + new ListItem(new SampleMarkdownPage()) + { + Title = "Markdown Page Sample Command", + Subtitle = "Display a page of rendered markdown", + }, + new ListItem(new SampleMarkdownManyBodies()) + { + Title = "Markdown with multiple blocks", + Subtitle = "A page with multiple blocks of rendered markdown", + }, + new ListItem(new SampleMarkdownDetails()) + { + Title = "Markdown with details", + Subtitle = "A page with markdown and details", + }, + + // Settings helpers + new ListItem(new SampleSettingsPage()) + { + Title = "Sample settings page", + Subtitle = "A demo of the settings helpers", + }, + + // Evil edge cases + // Anything weird that might break the palette - put that in here. + new ListItem(new EvilSamplesPage()) + { + Title = "Evil samples", + Subtitle = "Samples designed to break the palette in many different evil ways", + } + ]; + + public SamplesListPage() + { + Name = "Samples"; + Icon = new IconInfo("\ue946"); // Info + } + + public override IListItem[] GetItems() => _commands; +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/SelfImmolateCommand.cs b/src/modules/cmdpal/Exts/SamplePagesExtension/SelfImmolateCommand.cs new file mode 100644 index 0000000000..0f767082fb --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/SelfImmolateCommand.cs @@ -0,0 +1,18 @@ +// 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; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +public partial class SelfImmolateCommand : InvokableCommand +{ + public override ICommandResult Invoke() + { + Process.GetCurrentProcess().Kill(); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/SamplePagesExtension/app.manifest b/src/modules/cmdpal/Exts/SamplePagesExtension/app.manifest new file mode 100644 index 0000000000..6a7c7d68a7 --- /dev/null +++ b/src/modules/cmdpal/Exts/SamplePagesExtension/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Invoke-XamlFormat.ps1 b/src/modules/cmdpal/Invoke-XamlFormat.ps1 new file mode 100644 index 0000000000..88dbeb51c2 --- /dev/null +++ b/src/modules/cmdpal/Invoke-XamlFormat.ps1 @@ -0,0 +1,5 @@ +$gitRoot = git rev-parse --show-toplevel + +# $xamlFilesForStyler = (git ls-files "$gitRoot/**/*.xaml") -join "," +$xamlFilesForStyler = (git ls-files "$gitRoot/src/modules/cmdpal/**/*.xaml") -join "," +dotnet tool run xstyler -- -c "$gitRoot\src\Settings.XamlStyler" -f "$xamlFilesForStyler" \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs new file mode 100644 index 0000000000..b9348eb520 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Common.Contracts; + +public interface IFileService +{ + T Read(string folderPath, string fileName); + + void Save(string folderPath, string fileName, T content); + + void Delete(string folderPath, string fileName); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs new file mode 100644 index 0000000000..2350050e3e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace Microsoft.CmdPal.Common.Contracts; + +public interface ILocalSettingsService +{ + Task HasSettingAsync(string key); + + Task ReadSettingAsync(string key); + + Task SaveSettingAsync(string key, T value); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs new file mode 100644 index 0000000000..a975083c7c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Common.Services; +using Microsoft.UI.Xaml; + +namespace Microsoft.CmdPal.Common.Extensions; + +/// +/// Extension class implementing extension methods for . +/// +public static class ApplicationExtensions +{ + /// + /// Get registered services at the application level from anywhere in the + /// application. + /// + /// Note: + /// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current + /// "Application is a singleton that implements the static Current property + /// to provide shared access to the Application instance for the current + /// application. The singleton pattern ensures that state managed by + /// Application, including shared resources and properties, is available + /// from a single, shared location." + /// + /// Example of usage: + /// + /// Application.Current.GetService() + /// + /// + /// Service type. + /// Current application. + /// Service reference. + public static T GetService(this Application application) + where T : class + { + return (application as IApp)!.GetService(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs new file mode 100644 index 0000000000..660dcd2931 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.CmdPal.Common.Extensions; + +public static class IHostExtensions +{ + /// + /// + /// + public static T CreateInstance(this IHost host, params object[] parameters) + { + return ActivatorUtilities.CreateInstance(host.Services, parameters); + } + + /// + /// Gets the service object for the specified type, or throws an exception + /// if type was not registered. + /// + /// Service type + /// Host object + /// Service object + /// Throw an exception if the specified + /// type is not registered + public static T GetService(this IHost host) + where T : class + { + if (host.Services.GetService(typeof(T)) is not T service) + { + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices."); + } + + return service; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs new file mode 100644 index 0000000000..25ff815a69 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Common; + +public partial class ExtensionHostInstance +{ + public IExtensionHost? Host { get; private set; } + + public void Initialize(IExtensionHost host) => Host = host; + + /// + /// Fire-and-forget a log message to the Command Palette host app. Since + /// the host is in another process, we do this in a try/catch in a + /// background thread, as to not block the calling thread, nor explode if + /// the host app is gone. + /// + /// The log message to send + public void LogMessage(ILogMessage message) + { + if (Host != null) + { + _ = Task.Run(async () => + { + try + { + await Host.LogMessage(message); + } + catch (Exception) + { + } + }); + } + } + + public void LogMessage(string message) + { + var logMessage = new LogMessage() { Message = message }; + LogMessage(logMessage); + } + + public void ShowStatus(IStatusMessage message, StatusContext context) + { + if (Host != null) + { + _ = Task.Run(async () => + { + try + { + await Host.ShowStatus(message, context); + } + catch (Exception) + { + } + }); + } + } + + public void HideStatus(IStatusMessage message) + { + if (Host != null) + { + _ = Task.Run(async () => + { + try + { + await Host.HideStatus(message); + } + catch (Exception) + { + } + }); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs new file mode 100644 index 0000000000..d865e10bdb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Microsoft.CmdPal.Common.Helpers; + +public static class Json +{ + public static async Task ToObjectAsync(string value) + { + if (typeof(T) == typeof(bool)) + { + return (T)(object)bool.Parse(value); + } + + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value)); + return (await JsonSerializer.DeserializeAsync(stream))!; + } + + public static async Task StringifyAsync(T value) + { + if (typeof(T) == typeof(bool)) + { + return value!.ToString()!.ToLowerInvariant(); + } + + await using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, value); + return Encoding.UTF8.GetString(stream.ToArray()); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs new file mode 100644 index 0000000000..6ec1885a4c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; + +using Microsoft.UI.Dispatching; + +namespace Microsoft.CmdPal.Common.Helpers; + +public static class NativeEventWaiter +{ + public static void WaitForEventLoop(string eventName, Action callback) + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + var t = new Thread(() => + { + var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + while (true) + { + if (eventHandle.WaitOne()) + { + dispatcherQueue.TryEnqueue(() => callback()); + } + } + }); + + t.IsBackground = true; + t.Start(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs new file mode 100644 index 0000000000..46dce07e5e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs @@ -0,0 +1,56 @@ +// 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.Security.Principal; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.CmdPal.Common.Helpers; + +public static class RuntimeHelper +{ + public static bool IsMSIX + { + get + { + // TODO: for whatever reason, when I ported this into the PT + // codebase, this no longer compiled. We're only ever using it for + // the hacked up settings and ignoring it anyways, so I'm leaving + // it commented out for now. + // + // See also: + // * https://github.com/microsoft/win32metadata/commit/6fee67ba73bfe1b126ce524f7de8d367f0317715 + // * https://github.com/microsoft/win32metadata/issues/1311 + // uint length = 0; + // return PInvoke.GetCurrentPackageFullName(ref length, null) != WIN32_ERROR.APPMODEL_ERROR_NO_PACKAGE; +#pragma warning disable IDE0025 // Use expression body for property + return true; +#pragma warning restore IDE0025 // Use expression body for property + } + } + + public static bool IsOnWindows11 + { + get + { + var version = Environment.OSVersion.Version; + return version.Major >= 10 && version.Build >= 22000; + } + } + + public static bool IsCurrentProcessRunningAsAdmin() + { + var identity = WindowsIdentity.GetCurrent(); + return identity.Owner?.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid) ?? false; + } + + public static void VerifyCurrentProcessRunningAsAdmin() + { + if (!IsCurrentProcessRunningAsAdmin()) + { + throw new UnauthorizedAccessException("This operation requires elevated privileges."); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs new file mode 100644 index 0000000000..ed698d1024 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Common.Messages; + +public record HideWindowMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj new file mode 100644 index 0000000000..970df0df58 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj @@ -0,0 +1,33 @@ + + + + Microsoft.CmdPal.Common + enable + true + preview + + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\ + false + false + + Microsoft.CmdPal.Common.pri + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs new file mode 100644 index 0000000000..bae7422878 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Common.Models; + +public class LocalSettingsOptions +{ + public string? ApplicationDataFolder + { + get; set; + } + + public string? LocalSettingsFile + { + get; set; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt new file mode 100644 index 0000000000..0d456bde31 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt @@ -0,0 +1,20 @@ +EnableWindow +CoCreateInstance +FileOpenDialog +FileSaveDialog +IFileOpenDialog +IFileSaveDialog +SHCreateItemFromParsingName +GetCurrentPackageFullName +SetWindowLong +GetWindowLong +WINDOW_EX_STYLE +SHLoadIndirectString +StrFormatByteSizeEx +SFBS_FLAGS +MAX_PATH +GetDpiForWindow +GetWindowRect +GetMonitorInfo +SetWindowPos +MonitorFromWindow diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs new file mode 100644 index 0000000000..cc6ef96098 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Text; +using System.Text.Json; +using Microsoft.CmdPal.Common.Contracts; + +namespace Microsoft.CmdPal.Common.Services; + +public class FileService : IFileService +{ + private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + +#pragma warning disable CS8603 // Possible null reference return. + public T Read(string folderPath, string fileName) + { + var path = Path.Combine(folderPath, fileName); + if (File.Exists(path)) + { + using var fileStream = File.OpenText(path); + return JsonSerializer.Deserialize(fileStream.BaseStream); + } + + return default; + } +#pragma warning restore CS8603 // Possible null reference return. + + public void Save(string folderPath, string fileName, T content) + { + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + var fileContent = JsonSerializer.Serialize(content); + File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding); + } + + public void Delete(string folderPath, string fileName) + { + if (fileName != null && File.Exists(Path.Combine(folderPath, fileName))) + { + File.Delete(Path.Combine(folderPath, fileName)); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs new file mode 100644 index 0000000000..92980dfaff --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Common.Services; + +/// +/// Interface for the current application singleton object exposing the API +/// that can be accessed from anywhere in the application. +/// +public interface IApp +{ + /// + /// Gets services registered at the application level. + /// + public T GetService() + where T : class; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs new file mode 100644 index 0000000000..538b281be6 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Common.Services; + +public interface IExtensionService +{ + Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false); + + // Task> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false); + Task> GetInstalledExtensionsAsync(Microsoft.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false); + + IExtensionWrapper? GetInstalledExtension(string extensionUniqueId); + + Task SignalStopExtensionsAsync(); + + public event TypedEventHandler>? OnExtensionAdded; + + public event TypedEventHandler>? OnExtensionRemoved; + + public void EnableExtension(string extensionUniqueId); + + public void DisableExtension(string extensionUniqueId); + + ///// + ///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature + ///// being absent from the machine or in an unknown state. + ///// + ///// The out of proc extension object + ///// True only if the extension was disabled. False otherwise. + // public Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionWrapper.cs new file mode 100644 index 0000000000..61667366ba --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionWrapper.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Windows.ApplicationModel; + +namespace Microsoft.CmdPal.Common.Services; + +public interface IExtensionWrapper +{ + /// + /// Gets the DisplayName of the package as mentioned in the manifest + /// + string PackageDisplayName { get; } + + /// + /// Gets DisplayName of the extension as mentioned in the manifest + /// + string ExtensionDisplayName { get; } + + /// + /// Gets PackageFullName of the extension + /// + string PackageFullName { get; } + + /// + /// Gets PackageFamilyName of the extension + /// + string PackageFamilyName { get; } + + /// + /// Gets Publisher of the extension + /// + string Publisher { get; } + + /// + /// Gets class id (GUID) of the extension class (which implements IExtension) as mentioned in the manifest + /// + string ExtensionClassId { get; } + + /// + /// Gets the date on which the application package was installed or last updated. + /// + DateTimeOffset InstalledDate { get; } + + /// + /// Gets the PackageVersion of the extension + /// + PackageVersion Version { get; } + + /// + /// Gets the Unique Id for the extension + /// + public string ExtensionUniqueId { get; } + + /// + /// Checks whether we have a reference to the extension process and we are able to call methods on the interface. + /// + /// Whether we have a reference to the extension process and we are able to call methods on the interface. + bool IsRunning(); + + /// + /// Starts the extension if not running + /// + /// An awaitable task + Task StartExtensionAsync(); + + /// + /// Signals the extension to dispose itself and removes the reference to the extension com object + /// + void SignalDispose(); + + /// + /// Gets the underlying instance of IExtension + /// + /// Instance of IExtension + IExtension? GetExtensionObject(); + + /// + /// Tells the wrapper that the extension implements the given provider + /// + /// The type of provider to be added + void AddProviderType(ProviderType providerType); + + /// + /// Checks whether the given provider was added through `AddProviderType` method + /// + /// The type of the provider to be checked for + /// Whether the given provider was added through `AddProviderType` method + bool HasProviderType(ProviderType providerType); + + /// + /// Starts the extension if not running and gets the provider from the underlying IExtension object + /// Can be null if not found + /// + /// The type of provider + /// Nullable instance of the provider + Task GetProviderAsync() + where T : class; + + /// + /// Starts the extension if not running and gets a list of providers of type T from the underlying IExtension object. + /// If no providers are found, returns an empty list. + /// + /// The type of provider + /// Nullable instance of the provider + Task> GetListOfProvidersAsync() + where T : class; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs new file mode 100644 index 0000000000..e4cd2a174b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.CmdPal.Common.Contracts; +using Microsoft.CmdPal.Common.Helpers; +using Microsoft.CmdPal.Common.Models; +using Microsoft.Extensions.Options; +using Windows.Storage; + +namespace Microsoft.CmdPal.Common.Services; + +public class LocalSettingsService : ILocalSettingsService +{ + // TODO! for now, we're hardcoding the path as effectively: + // %localappdata%\CmdPal\LocalSettings.json + private const string DefaultApplicationDataFolder = "CmdPal"; + private const string DefaultLocalSettingsFile = "LocalSettings.json"; + + private readonly IFileService _fileService; + private readonly LocalSettingsOptions _options; + + private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + private readonly string _applicationDataFolder; + private readonly string _localSettingsFile; + + private readonly bool _isMsix; + + private Dictionary _settings; + private bool _isInitialized; + + public LocalSettingsService(IFileService fileService, IOptions options) + { + _isMsix = false; // RuntimeHelper.IsMSIX; + + _fileService = fileService; + _options = options.Value; + + _applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder); + _localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile; + + _settings = new Dictionary(); + } + + private async Task InitializeAsync() + { + if (!_isInitialized) + { + _settings = await Task.Run(() => _fileService.Read>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary(); + + _isInitialized = true; + } + } + + public async Task HasSettingAsync(string key) + { + if (_isMsix) + { + return ApplicationData.Current.LocalSettings.Values.ContainsKey(key); + } + else + { + await InitializeAsync(); + + if (_settings != null) + { + return _settings.ContainsKey(key); + } + } + + return false; + } + + public async Task ReadSettingAsync(string key) + { + if (_isMsix) + { + if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj)) + { + return await Json.ToObjectAsync((string)obj); + } + } + else + { + await InitializeAsync(); + + if (_settings != null && _settings.TryGetValue(key, out var obj)) + { + var s = obj.ToString(); + + if (s != null) + { + return await Json.ToObjectAsync(s); + } + } + } + + return default; + } + + public async Task SaveSettingAsync(string key, T value) + { + if (_isMsix) + { + ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!); + } + else + { + await InitializeAsync(); + + _settings[key] = await Json.StringifyAsync(value!); + + await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings)); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs new file mode 100644 index 0000000000..2d5fd66145 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs @@ -0,0 +1,120 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class AliasManager : ObservableObject +{ + private readonly TopLevelCommandManager _topLevelCommandManager; + + // REMEMBER, CommandAlias.SearchPrefix is what we use as keys + private readonly Dictionary _aliases; + + public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings) + { + _topLevelCommandManager = tlcManager; + _aliases = settings.Aliases; + + if (_aliases.Count == 0) + { + PopulateDefaultAliases(); + } + } + + private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a); + + public bool CheckAlias(string searchText) + { + if (_aliases.TryGetValue(searchText, out var alias)) + { + try + { + var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId); + if (topLevelCommand != null) + { + WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new(topLevelCommand)); + return true; + } + } + catch + { + } + } + + return false; + } + + private void PopulateDefaultAliases() + { + this.AddAlias(new CommandAlias(":", "com.microsoft.cmdpal.registry", true)); + this.AddAlias(new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true)); + this.AddAlias(new CommandAlias("=", "com.microsoft.cmdpal.calculator", true)); + this.AddAlias(new CommandAlias(">", "com.microsoft.cmdpal.shell", true)); + this.AddAlias(new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true)); + this.AddAlias(new CommandAlias("??", "com.microsoft.cmdpal.websearch", true)); + this.AddAlias(new CommandAlias("file", "com.microsoft.indexer.fileSearch", false)); + this.AddAlias(new CommandAlias(")", "com.microsoft.cmdpal.timedate", true)); + } + + public string? KeysFromId(string commandId) + { + return _aliases + .Where(kv => kv.Value.CommandId == commandId) + .Select(kv => kv.Value.Alias) + .FirstOrDefault(); + } + + public CommandAlias? AliasFromId(string commandId) + { + return _aliases + .Where(kv => kv.Value.CommandId == commandId) + .Select(kv => kv.Value) + .FirstOrDefault(); + } + + public void UpdateAlias(string commandId, CommandAlias? newAlias) + { + if (string.IsNullOrEmpty(commandId)) + { + // do nothing? + return; + } + + // If we already have _this exact alias_, do nothing + if (newAlias != null && + _aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias)) + { + if (existingAlias.CommandId == commandId) + { + return; + } + } + + // Look for the old alias, and remove it + List toRemove = []; + foreach (var kv in _aliases) + { + if (kv.Value.CommandId == commandId) + { + toRemove.Add(kv.Value); + } + } + + foreach (var alias in toRemove) + { + // REMEMBER, SearchPrefix is what we use as keys + _aliases.Remove(alias.SearchPrefix); + } + + if (newAlias != null) + { + AddAlias(newAlias); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs new file mode 100644 index 0000000000..69d38a8655 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs @@ -0,0 +1,139 @@ +// 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; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class AppStateModel : ObservableObject +{ + [JsonIgnore] + public static readonly string FilePath; + + public event TypedEventHandler? StateChanged; + + /////////////////////////////////////////////////////////////////////////// + // STATE HERE + public RecentCommandsManager RecentCommands { get; private set; } = new(); + + // END SETTINGS + /////////////////////////////////////////////////////////////////////////// + + static AppStateModel() + { + FilePath = StateJsonPath(); + } + + public static AppStateModel LoadState() + { + if (string.IsNullOrEmpty(FilePath)) + { + throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadState)}"); + } + + if (!File.Exists(FilePath)) + { + Debug.WriteLine("The provided settings file does not exist"); + return new(); + } + + try + { + // Read the JSON content from the file + var jsonContent = File.ReadAllText(FilePath); + + var loaded = JsonSerializer.Deserialize(jsonContent, _deserializerOptions); + + Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + + return loaded ?? new(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + + return new(); + } + + public static void SaveState(AppStateModel model) + { + if (string.IsNullOrEmpty(FilePath)) + { + throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}"); + } + + try + { + // Serialize the main dictionary to JSON and save it to the file + var settingsJson = JsonSerializer.Serialize(model, _serializerOptions); + + // Is it valid JSON? + if (JsonNode.Parse(settingsJson) is JsonObject newSettings) + { + // Now, read the existing content from the file + var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}"; + + // Is it valid JSON? + if (JsonNode.Parse(oldContent) is JsonObject savedSettings) + { + foreach (var item in newSettings) + { + savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null; + } + + var serialized = savedSettings.ToJsonString(_serializerOptions); + File.WriteAllText(FilePath, serialized); + + // TODO: Instead of just raising the event here, we should + // have a file change watcher on the settings file, and + // reload the settings then + model.StateChanged?.Invoke(model, null); + } + else + { + Debug.WriteLine("Failed to parse settings file as JsonObject."); + } + } + else + { + Debug.WriteLine("Failed to parse settings file as JsonObject."); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + } + + internal static string StateJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the settings is just next to the exe + return Path.Combine(directory, "state.json"); + } + + private static readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() }, + }; + + private static readonly JsonSerializerOptions _deserializerOptions = new() + { + PropertyNameCaseInsensitive = true, + IncludeFields = true, + AllowTrailingCommas = true, + PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, + ReadCommentHandling = JsonCommentHandling.Skip, + }; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Assets/template.zip b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Assets/template.zip new file mode 100644 index 0000000000..f136718614 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Assets/template.zip differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandAlias.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandAlias.cs new file mode 100644 index 0000000000..7179ac7660 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandAlias.cs @@ -0,0 +1,38 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class CommandAlias +{ + public string CommandId { get; set; } + + public string Alias { get; set; } + + public bool IsDirect { get; set; } + + [JsonIgnore] + public string SearchPrefix => Alias + (IsDirect ? string.Empty : " "); + + public CommandAlias(string shortcut, string commandId, bool direct = false) + { + CommandId = commandId; + Alias = shortcut; + IsDirect = direct; + } + + public CommandAlias() + : this(string.Empty, string.Empty, false) + { + } + + public static CommandAlias FromSearchText(string text, string commandId) + { + var trailingSpace = text.EndsWith(' '); + var realAlias = trailingSpace ? text.Substring(0, text.Length - 1) : text; + return new CommandAlias(realAlias, commandId, !trailingSpace); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs new file mode 100644 index 0000000000..9b5be8a973 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class CommandBarViewModel : ObservableObject, + IRecipient +{ + public ICommandBarContext? SelectedItem + { + get => field; + set + { + if (field != null) + { + field.PropertyChanged -= SelectedItemPropertyChanged; + } + + field = value; + SetSelectedItem(value); + } + } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasPrimaryCommand))] + public partial CommandItemViewModel? PrimaryCommand { get; set; } + + public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasSecondaryCommand))] + public partial CommandItemViewModel? SecondaryCommand { get; set; } + + public bool HasSecondaryCommand => SecondaryCommand != null; + + [ObservableProperty] + public partial bool ShouldShowContextMenu { get; set; } = false; + + [ObservableProperty] + public partial PageViewModel? CurrentPage { get; set; } + + [ObservableProperty] + public partial ObservableCollection ContextCommands { get; set; } = []; + + public CommandBarViewModel() + { + WeakReferenceMessenger.Default.Register(this); + } + + public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel; + + private void SetSelectedItem(ICommandBarContext? value) + { + if (value != null) + { + PrimaryCommand = value.PrimaryCommand; + value.PropertyChanged += SelectedItemPropertyChanged; + } + else + { + if (SelectedItem != null) + { + SelectedItem.PropertyChanged -= SelectedItemPropertyChanged; + } + + PrimaryCommand = null; + } + + UpdateContextItems(); + } + + private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(SelectedItem.HasMoreCommands): + UpdateContextItems(); + break; + } + } + + private void UpdateContextItems() + { + if (SelectedItem == null) + { + SecondaryCommand = null; + ShouldShowContextMenu = false; + return; + } + + SecondaryCommand = SelectedItem.SecondaryCommand; + + if (SelectedItem.MoreCommands.Count() > 1) + { + ShouldShowContextMenu = true; + ContextCommands = [.. SelectedItem.AllCommands]; + } + else + { + ShouldShowContextMenu = false; + } + } + + // InvokeItemCommand is what this will be in Xaml due to source generator + // this comes in when an item in the list is tapped + [RelayCommand] + private void InvokeItem(CommandContextItemViewModel item) => + WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); + + // this comes in when the primary button is tapped + public void InvokePrimaryCommand() + { + if (PrimaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); + } + } + + // this comes in when the secondary button is tapped + public void InvokeSecondaryCommand() + { + if (SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs new file mode 100644 index 0000000000..f3475fd964 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs @@ -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.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) : CommandItemViewModel(new(contextItem), context) +{ + public new ExtensionObject Model { get; } = new(contextItem); + + public bool IsCritical { get; private set; } + + public KeyChord? RequestedShortcut { get; private set; } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + base.InitializeProperties(); + + var contextItem = Model.Unsafe; + if (contextItem == null) + { + return; // throw? + } + + IsCritical = contextItem.IsCritical; + if (contextItem.RequestedShortcut != null) + { + RequestedShortcut = new( + contextItem.RequestedShortcut.Modifiers, + contextItem.RequestedShortcut.Vkey, + contextItem.RequestedShortcut.ScanCode); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs new file mode 100644 index 0000000000..87e5888e1a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -0,0 +1,408 @@ +// 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.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext +{ + public ExtensionObject Model => _commandItemModel; + + private readonly ExtensionObject _commandItemModel = new(null); + private CommandContextItemViewModel? _defaultCommandContextItem; + + internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized; + + protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized); + + protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized); + + protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized); + + public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error); + + // These are properties that are "observable" from the extension object + // itself, in the sense that they get raised by PropChanged events from the + // extension. However, we don't want to actually make them + // [ObservableProperty]s, because PropChanged comes in off the UI thread, + // and ObservableProperty is not smart enough to raise the PropertyChanged + // on the UI thread. + public string Name => Command.Name; + + private string _itemTitle = string.Empty; + + public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle; + + public string Subtitle { get; private set; } = string.Empty; + + private IconInfoViewModel _listItemIcon = new(null); + + public IconInfoViewModel Icon => _listItemIcon.IsSet ? _listItemIcon : Command.Icon; + + public CommandViewModel Command { get; private set; } + + public List MoreCommands { get; private set; } = []; + + IEnumerable ICommandBarContext.MoreCommands => MoreCommands; + + public bool HasMoreCommands => MoreCommands.Count > 0; + + public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty; + + public CommandItemViewModel? PrimaryCommand => this; + + public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? MoreCommands[0] : null; + + public bool ShouldBeVisible => !string.IsNullOrEmpty(Name); + + public List AllCommands + { + get + { + List l = _defaultCommandContextItem == null ? + new() : + [_defaultCommandContextItem]; + + l.AddRange(MoreCommands); + return l; + } + } + + private static readonly IconInfoViewModel _errorIcon; + + static CommandItemViewModel() + { + _errorIcon = new(new IconInfo("\uEA39")); // ErrorBadge + _errorIcon.InitializeProperties(); + } + + public CommandItemViewModel(ExtensionObject item, WeakReference errorContext) + : base(errorContext) + { + _commandItemModel = item; + Command = new(null, errorContext); + } + + public void FastInitializeProperties() + { + if (IsFastInitialized) + { + return; + } + + var model = _commandItemModel.Unsafe; + if (model == null) + { + return; + } + + Command = new(model.Command, PageContext); + Command.FastInitializeProperties(); + + _itemTitle = model.Title; + Subtitle = model.Subtitle; + + Initialized |= InitializedState.FastInitialized; + } + + //// Called from ListViewModel on background thread started in ListPage.xaml.cs + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + if (!IsFastInitialized) + { + FastInitializeProperties(); + } + + var model = _commandItemModel.Unsafe; + if (model == null) + { + return; + } + + Command.InitializeProperties(); + + var listIcon = model.Icon; + if (listIcon != null) + { + _listItemIcon = new(listIcon); + _listItemIcon.InitializeProperties(); + } + + // TODO: Do these need to go into FastInit? + model.PropChanged += Model_PropChanged; + Command.PropertyChanged += Command_PropertyChanged; + + UpdateProperty(nameof(Name)); + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(Subtitle)); + UpdateProperty(nameof(Icon)); + UpdateProperty(nameof(IsInitialized)); + + Initialized |= InitializedState.Initialized; + } + + public void SlowInitializeProperties() + { + if (IsSelectedInitialized) + { + return; + } + + if (!IsInitialized) + { + InitializeProperties(); + } + + var model = _commandItemModel.Unsafe; + if (model == null) + { + return; + } + + var more = model.MoreCommands; + if (more != null) + { + MoreCommands = more + .Where(contextItem => contextItem is ICommandContextItem) + .Select(contextItem => (contextItem as ICommandContextItem)!) + .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) + .ToList(); + } + + // Here, we're already theoretically in the async context, so we can + // use Initialize straight up + MoreCommands.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); + + _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 + }; + + // Only set the icon on the context item for us if our command didn't + // have its own icon + if (!Command.HasIcon) + { + _defaultCommandContextItem._listItemIcon = _listItemIcon; + } + + Initialized |= InitializedState.SelectionInitialized; + UpdateProperty(nameof(MoreCommands)); + UpdateProperty(nameof(AllCommands)); + UpdateProperty(nameof(IsSelectedInitialized)); + } + + public bool SafeFastInit() + { + try + { + FastInitializeProperties(); + return true; + } + catch (Exception) + { + Command = new(null, PageContext); + _itemTitle = "Error"; + Subtitle = "Item failed to load"; + MoreCommands = []; + _listItemIcon = _errorIcon; + Initialized |= InitializedState.Error; + } + + return false; + } + + public bool SafeSlowInit() + { + try + { + SlowInitializeProperties(); + return true; + } + catch (Exception) + { + Initialized |= InitializedState.Error; + } + + return false; + } + + public bool SafeInitializeProperties() + { + try + { + InitializeProperties(); + return true; + } + catch (Exception) + { + Command = new(null, PageContext); + _itemTitle = "Error"; + Subtitle = "Item failed to load"; + MoreCommands = []; + _listItemIcon = _errorIcon; + Initialized |= InitializedState.Error; + } + + return false; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + FetchProperty(args.PropertyName); + } + catch (Exception ex) + { + ShowException(ex, _commandItemModel?.Unsafe?.Title); + } + } + + protected virtual void FetchProperty(string propertyName) + { + var model = this._commandItemModel.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Command): + if (Command != null) + { + Command.PropertyChanged -= Command_PropertyChanged; + } + + Command = new(model.Command, PageContext); + Command.InitializeProperties(); + UpdateProperty(nameof(Name)); + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(Icon)); + break; + + case nameof(Title): + _itemTitle = model.Title; + break; + + case nameof(Subtitle): + this.Subtitle = model.Subtitle; + break; + + case nameof(Icon): + _listItemIcon = new(model.Icon); + _listItemIcon.InitializeProperties(); + break; + + case nameof(model.MoreCommands): + var more = model.MoreCommands; + if (more != null) + { + var newContextMenu = more + .Where(contextItem => contextItem is ICommandContextItem) + .Select(contextItem => (contextItem as ICommandContextItem)!) + .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) + .ToList(); + lock (MoreCommands) + { + ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu); + } + + newContextMenu.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); + } + else + { + lock (MoreCommands) + { + MoreCommands.Clear(); + } + } + + UpdateProperty(nameof(SecondaryCommand)); + UpdateProperty(nameof(SecondaryCommandName)); + UpdateProperty(nameof(HasMoreCommands)); + + break; + } + + UpdateProperty(propertyName); + } + + private void Command_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var propertyName = e.PropertyName; + switch (propertyName) + { + case nameof(Command.Name): + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(Name)); + break; + case nameof(Command.Icon): + UpdateProperty(nameof(Icon)); + break; + } + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + lock (MoreCommands) + { + MoreCommands.ForEach(c => c.SafeCleanup()); + MoreCommands.Clear(); + } + + // _listItemIcon.SafeCleanup(); + _listItemIcon = new(null); // necessary? + + _defaultCommandContextItem?.SafeCleanup(); + _defaultCommandContextItem = null; + + Command.PropertyChanged -= Command_PropertyChanged; + Command.SafeCleanup(); + + var model = _commandItemModel.Unsafe; + if (model != null) + { + model.PropChanged -= Model_PropChanged; + } + } + + public override void SafeCleanup() + { + base.SafeCleanup(); + Initialized |= InitializedState.CleanedUp; + } +} + +[Flags] +internal enum InitializedState +{ + Uninitialized = 0, + FastInitialized = 1, + Initialized = 2, + SelectionInitialized = 4, + Error = 8, + CleanedUp = 16, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs new file mode 100644 index 0000000000..9aa12dc037 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using System.Diagnostics; +using ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public sealed partial class CommandPaletteHost : IExtensionHost +{ + // Static singleton, so that we can access this from anywhere + // Post MVVM - this should probably be like, a dependency injection thing. + public static CommandPaletteHost Instance { get; } = new(); + + private static readonly GlobalLogPageContext _globalLogPageContext = new(); + + private static ulong _hostingHwnd; + + public ulong HostingHwnd => _hostingHwnd; + + public string LanguageOverride => string.Empty; + + public static ObservableCollection LogMessages { get; } = []; + + public ObservableCollection StatusMessages { get; } = []; + + public IExtensionWrapper? Extension { get; } + + private readonly ICommandProvider? _builtInProvider; + + private CommandPaletteHost() + { + } + + public CommandPaletteHost(IExtensionWrapper source) + { + Extension = source; + } + + public CommandPaletteHost(ICommandProvider builtInProvider) + { + _builtInProvider = builtInProvider; + } + + public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context) + { + if (message == null) + { + return Task.CompletedTask.AsAsyncAction(); + } + + Debug.WriteLine(message.Message); + + _ = Task.Run(() => + { + ProcessStatusMessage(message, context); + }); + + return Task.CompletedTask.AsAsyncAction(); + } + + public IAsyncAction HideStatus(IStatusMessage? message) + { + if (message == null) + { + return Task.CompletedTask.AsAsyncAction(); + } + + _ = Task.Run(() => + { + ProcessHideStatusMessage(message); + }); + return Task.CompletedTask.AsAsyncAction(); + } + + public IAsyncAction LogMessage(ILogMessage? message) + { + if (message == null) + { + return Task.CompletedTask.AsAsyncAction(); + } + + Logger.LogDebug(message.Message); + + _ = Task.Run(() => + { + ProcessLogMessage(message); + }); + + // We can't just make a LogMessageViewModel : ExtensionObjectViewModel + // because we don't necessarily know the page context. Butts. + return Task.CompletedTask.AsAsyncAction(); + } + + public void ProcessLogMessage(ILogMessage message) + { + var vm = new LogMessageViewModel(message, _globalLogPageContext); + vm.SafeInitializePropertiesSynchronous(); + + if (Extension != null) + { + vm.ExtensionPfn = Extension.PackageFamilyName; + } + + Task.Factory.StartNew( + () => + { + LogMessages.Add(vm); + }, + CancellationToken.None, + TaskCreationOptions.None, + _globalLogPageContext.Scheduler); + } + + public void ProcessStatusMessage(IStatusMessage message, StatusContext context) + { + // If this message is already in the list of messages, just bring it to the top + var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); + if (oldVm != null) + { + Task.Factory.StartNew( + () => + { + StatusMessages.Remove(oldVm); + StatusMessages.Add(oldVm); + }, + CancellationToken.None, + TaskCreationOptions.None, + _globalLogPageContext.Scheduler); + return; + } + + var vm = new StatusMessageViewModel(message, new(_globalLogPageContext)); + vm.SafeInitializePropertiesSynchronous(); + + if (Extension != null) + { + vm.ExtensionPfn = Extension.PackageFamilyName; + } + + Task.Factory.StartNew( + () => + { + StatusMessages.Add(vm); + }, + CancellationToken.None, + TaskCreationOptions.None, + _globalLogPageContext.Scheduler); + } + + public void ProcessHideStatusMessage(IStatusMessage message) + { + Task.Factory.StartNew( + () => + { + try + { + var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); + if (vm != null) + { + StatusMessages.Remove(vm); + } + } + catch + { + } + }, + CancellationToken.None, + TaskCreationOptions.None, + _globalLogPageContext.Scheduler); + } + + public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd; + + public void DebugLog(string message) + { +#if DEBUG + this.ProcessLogMessage(new LogMessage(message)); +#endif + } + + public void Log(string message) + { + this.ProcessLogMessage(new LogMessage(message)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs new file mode 100644 index 0000000000..7e712e5eb5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -0,0 +1,185 @@ +// 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 ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public sealed class CommandProviderWrapper +{ + public bool IsExtension => Extension != null; + + private readonly bool isValid; + + private readonly ExtensionObject _commandProvider; + + private readonly TaskScheduler _taskScheduler; + + public ICommandItem[] TopLevelItems { get; private set; } = []; + + public IFallbackCommandItem[] FallbackItems { get; private set; } = []; + + public string DisplayName { get; private set; } = string.Empty; + + public IExtensionWrapper? Extension { get; } + + public CommandPaletteHost ExtensionHost { get; private set; } + + public event TypedEventHandler? CommandsChanged; + + public string Id { get; private set; } = string.Empty; + + public IconInfoViewModel Icon { get; private set; } = new(null); + + public CommandSettingsViewModel? Settings { get; private set; } + + public string ProviderId => $"{Extension?.PackageFamilyName ?? string.Empty}/{Id}"; + + public CommandProviderWrapper(ICommandProvider provider, TaskScheduler mainThread) + { + // This ctor is only used for in-proc builtin commands. So the Unsafe! + // calls are pretty dang safe actually. + _commandProvider = new(provider); + _taskScheduler = mainThread; + + // Hook the extension back into us + ExtensionHost = new CommandPaletteHost(provider); + _commandProvider.Unsafe!.InitializeWithHost(ExtensionHost); + + _commandProvider.Unsafe!.ItemsChanged += CommandProvider_ItemsChanged; + + isValid = true; + Id = provider.Id; + DisplayName = provider.DisplayName; + Icon = new(provider.Icon); + Icon.InitializeProperties(); + Settings = new(provider.Settings, this, _taskScheduler); + Settings.InitializeProperties(); + + Logger.LogDebug($"Initialized command provider {ProviderId}"); + } + + public CommandProviderWrapper(IExtensionWrapper extension, TaskScheduler mainThread) + { + _taskScheduler = mainThread; + Extension = extension; + ExtensionHost = new CommandPaletteHost(extension); + if (!Extension.IsRunning()) + { + throw new ArgumentException("You forgot to start the extension. This is a CmdPal error - we need to make sure to call StartExtensionAsync"); + } + + var extensionImpl = extension.GetExtensionObject(); + var providerObject = extensionImpl?.GetProvider(ProviderType.Commands); + if (providerObject is not ICommandProvider provider) + { + throw new ArgumentException("extension didn't actually implement ICommandProvider"); + } + + _commandProvider = new(provider); + + try + { + var model = _commandProvider.Unsafe!; + + // Hook the extension back into us + model.InitializeWithHost(ExtensionHost); + model.ItemsChanged += CommandProvider_ItemsChanged; + + isValid = true; + + Logger.LogDebug($"Initialized extension command provider {Extension.PackageFamilyName}:{Extension.ExtensionUniqueId}"); + } + catch (Exception e) + { + Logger.LogError("Failed to initialize CommandProvider for extension."); + Logger.LogError($"Extension was {Extension!.PackageFamilyName}"); + Logger.LogError(e.ToString()); + } + + isValid = true; + } + + public async Task LoadTopLevelCommands() + { + if (!isValid) + { + return; + } + + ICommandItem[]? commands = null; + IFallbackCommandItem[]? fallbacks = null; + + try + { + var model = _commandProvider.Unsafe!; + + var t = new Task(model.TopLevelCommands); + t.Start(); + commands = await t.ConfigureAwait(false); + + // On a BG thread here + fallbacks = model.FallbackCommands(); + + Id = model.Id; + DisplayName = model.DisplayName; + Icon = new(model.Icon); + Icon.InitializeProperties(); + + Settings = new(model.Settings, this, _taskScheduler); + Settings.InitializeProperties(); + + Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})"); + } + catch (Exception e) + { + Logger.LogError("Failed to load commands from extension"); + Logger.LogError($"Extension was {Extension!.PackageFamilyName}"); + Logger.LogError(e.ToString()); + } + + if (commands != null) + { + TopLevelItems = commands; + } + + if (fallbacks != null) + { + FallbackItems = fallbacks; + } + } + + /* This is a View/ExtensionHost piece + * public void AllowSetForeground(bool allow) + { + if (!IsExtension) + { + return; + } + + var iextn = extensionWrapper?.GetExtensionObject(); + unsafe + { + PInvoke.CoAllowSetForegroundWindow(iextn); + } + }*/ + + public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid; + + public override int GetHashCode() => _commandProvider.GetHashCode(); + + private void CommandProvider_ItemsChanged(object sender, IItemsChangedEventArgs args) => + + // We don't want to handle this ourselves - we want the + // TopLevelCommandManager to know about this, so they can remove + // our old commands from their own list. + // + // In handling this, a call will be made to `LoadTopLevelCommands` to + // retrieve the new items. + this.CommandsChanged?.Invoke(this, args); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs new file mode 100644 index 0000000000..0853dbd932 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread) +{ + private readonly ExtensionObject _model = new(_unsafeSettings); + + public ContentPageViewModel? SettingsPage { get; private set; } + + public void InitializeProperties() + { + var model = _model.Unsafe; + if (model == null) + { + return; + } + + if (model.SettingsPage is IContentPage page) + { + SettingsPage = new(page, mainThread, provider.ExtensionHost); + SettingsPage.InitializeProperties(); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandViewModel.cs new file mode 100644 index 0000000000..b0cf589dd9 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandViewModel.cs @@ -0,0 +1,132 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class CommandViewModel : ExtensionObjectViewModel +{ + public ExtensionObject Model { get; private set; } = new(null); + + protected bool IsInitialized { get; private set; } + + protected bool IsFastInitialized { get; private set; } + + public bool HasIcon => Icon.IsSet; + + // These are properties that are "observable" from the extension object + // itself, in the sense that they get raised by PropChanged events from the + // extension. However, we don't want to actually make them + // [ObservableProperty]s, because PropChanged comes in off the UI thread, + // and ObservableProperty is not smart enough to raise the PropertyChanged + // on the UI thread. + public string Id { get; private set; } = string.Empty; + + public string Name { get; private set; } = string.Empty; + + public IconInfoViewModel Icon { get; private set; } + + public CommandViewModel(ICommand? command, WeakReference pageContext) + : base(pageContext) + { + Model = new(command); + Icon = new(null); + } + + public void FastInitializeProperties() + { + if (IsFastInitialized) + { + return; + } + + var model = Model.Unsafe; + if (model == null) + { + return; + } + + Name = model.Name ?? string.Empty; + IsFastInitialized = true; + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + if (!IsFastInitialized) + { + FastInitializeProperties(); + } + + var model = Model.Unsafe; + if (model == null) + { + return; + } + + var ico = model.Icon; + if (ico != null) + { + Icon = new(ico); + Icon.InitializeProperties(); + UpdateProperty(nameof(Icon)); + } + + model.PropChanged += Model_PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + FetchProperty(args.PropertyName); + } + catch (Exception ex) + { + ShowException(ex, Name); + } + } + + protected void FetchProperty(string propertyName) + { + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Name): + Name = model.Name; + break; + case nameof(Icon): + var iconInfo = model.Icon; + Icon = new(iconInfo); + Icon.InitializeProperties(); + break; + } + + UpdateProperty(propertyName); + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + Icon = new(null); // necessary? + + var model = Model.Unsafe; + if (model != null) + { + model.PropChanged -= Model_PropChanged; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs new file mode 100644 index 0000000000..9552e108ef --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs @@ -0,0 +1,43 @@ +// 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.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +/// +/// Built-in Provider for a top-level command which can quit the application. Invokes the , which sends a . +/// +public partial class BuiltInsCommandProvider : CommandProvider +{ + private readonly OpenSettingsCommand openSettings = new(); + private readonly QuitCommand quitCommand = new(); + private readonly FallbackReloadItem _fallbackReloadItem = new(); + private readonly FallbackLogItem _fallbackLogItem = new(); + private readonly NewExtensionPage _newExtension = new(); + + public override ICommandItem[] TopLevelCommands() => + [ + new CommandItem(openSettings) { Subtitle = Properties.Resources.builtin_open_settings_subtitle }, + new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle }, + ]; + + public override IFallbackCommandItem[] FallbackCommands() => + [ + new FallbackCommandItem(quitCommand, displayTitle: Properties.Resources.builtin_quit_subtitle) { Subtitle = Properties.Resources.builtin_quit_subtitle }, + _fallbackReloadItem, + _fallbackLogItem, + ]; + + public BuiltInsCommandProvider() + { + Id = "Core"; + DisplayName = Properties.Resources.builtin_display_name; + Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); + } + + public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltinsExtensionHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltinsExtensionHost.cs new file mode 100644 index 0000000000..f643f0fc84 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltinsExtensionHost.cs @@ -0,0 +1,20 @@ +// 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.IO.Compression; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.CmdPal.Common; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +public partial class BuiltinsExtensionHost +{ + internal static ExtensionHostInstance Instance { get; } = new(); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs new file mode 100644 index 0000000000..1939162662 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs @@ -0,0 +1,144 @@ +// 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.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +internal sealed partial class CreatedExtensionForm : NewExtensionFormBase +{ + public CreatedExtensionForm(string name, string displayName, string path) + { + TemplateJson = CardTemplate; + DataJson = $$""" +{ + "name": {{JsonSerializer.Serialize(name)}}, + "directory": {{JsonSerializer.Serialize(path)}}, + "displayName": {{JsonSerializer.Serialize(displayName)}} +} +"""; + _name = name; + _displayName = displayName; + _path = path; + } + + public override ICommandResult SubmitForm(string inputs, string data) + { + JsonObject? dataInput = JsonNode.Parse(data)?.AsObject(); + if (dataInput == null) + { + return CommandResult.KeepOpen(); + } + + string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty; + return verb switch + { + "sln" => OpenSolution(), + "dir" => OpenDirectory(), + "new" => CreateNew(), + _ => CommandResult.KeepOpen(), + }; + } + + private ICommandResult OpenSolution() + { + string[] parts = [_path, _name, $"{_name}.sln"]; + string pathToSolution = Path.Combine(parts); + ShellHelpers.OpenInShell(pathToSolution); + return CommandResult.Hide(); + } + + private ICommandResult OpenDirectory() + { + string[] parts = [_path, _name]; + string pathToDir = Path.Combine(parts); + ShellHelpers.OpenInShell(pathToDir); + return CommandResult.Hide(); + } + + private ICommandResult CreateNew() + { + RaiseFormSubmit(null); + return CommandResult.KeepOpen(); + } + + private static readonly string CardTemplate = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.6", + "body": [ + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_success}}", + "size": "large", + "weight": "bolder", + "style": "heading", + "wrap": true + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_created_in_text}}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "${directory}", + "fontType": "monospace" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_created_next_steps_title}}", + "style": "heading", + "wrap": true + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_created_next_steps}}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_created_next_steps_p2}}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_created_next_steps_p3}}", + "wrap": true + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "{{Properties.Resources.builtin_create_extension_open_solution}}", + "data": { + "x": "sln" + } + }, + { + "type": "Action.Submit", + "title": "{{Properties.Resources.builtin_create_extension_open_directory}}", + "data": { + "x": "dir" + } + }, + { + "type": "Action.Submit", + "title": "{{Properties.Resources.builtin_create_extension_create_another}}", + "data": { + "x": "new" + } + } + ] +} +"""; + + private readonly string _name; + private readonly string _displayName; + private readonly string _path; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs new file mode 100644 index 0000000000..686dadbfbb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs @@ -0,0 +1,29 @@ +// 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.UI.ViewModels.Commands; +using Microsoft.CmdPal.UI.ViewModels.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +internal sealed partial class FallbackLogItem : FallbackCommandItem +{ + private readonly LogMessagesPage _logMessagesPage; + + public FallbackLogItem() + : base(new LogMessagesPage(), Resources.builtin_log_subtitle) + { + _logMessagesPage = (LogMessagesPage)Command!; + Title = string.Empty; + _logMessagesPage.Name = string.Empty; + Subtitle = Properties.Resources.builtin_log_subtitle; + } + + public override void UpdateQuery(string query) + { + _logMessagesPage.Name = query.StartsWith('l') ? Properties.Resources.builtin_log_title : string.Empty; + Title = _logMessagesPage.Name; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs new file mode 100644 index 0000000000..22dfe77776 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +internal sealed partial class FallbackReloadItem : FallbackCommandItem +{ + private readonly ReloadExtensionsCommand _reloadCommand; + + public FallbackReloadItem() + : base(new ReloadExtensionsCommand(), Properties.Resources.builtin_reload_display_title) + { + _reloadCommand = (ReloadExtensionsCommand)Command!; + Title = string.Empty; + Subtitle = Properties.Resources.builtin_reload_subtitle; + } + + public override void UpdateQuery(string query) + { + _reloadCommand.Name = query.StartsWith('r') ? "Reload" : string.Empty; + Title = _reloadCommand.Name; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs new file mode 100644 index 0000000000..e0be3beb90 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Specialized; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.Commands; + +public partial class LogMessagesPage : ListPage +{ + private readonly List _listItems = new(); + + public LogMessagesPage() + { + Name = Properties.Resources.builtin_log_name; + Title = Properties.Resources.builtin_log_page_name; + Icon = new IconInfo("\uE8FD"); // BulletedList icon + CommandPaletteHost.LogMessages.CollectionChanged += LogMessages_CollectionChanged; + } + + private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + { + foreach (var item in e.NewItems) + { + if (item is LogMessageViewModel logMessageViewModel) + { + var li = new ListItem(new NoOpCommand()) + { + Title = logMessageViewModel.Message, + Subtitle = logMessageViewModel.ExtensionPfn, + }; + _listItems.Insert(0, li); + } + } + + RaiseItemsChanged(_listItems.Count); + } + } + + public override IListItem[] GetItems() + { + return _listItems.ToArray(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs new file mode 100644 index 0000000000..38187ad3f8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -0,0 +1,242 @@ +// 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.Specialized; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Ext.Apps; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels.MainPage; + +/// +/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a . +/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels. +/// +public partial class MainListPage : DynamicListPage, + IRecipient, + IRecipient +{ + private readonly IServiceProvider _serviceProvider; + + private readonly TopLevelCommandManager _tlcManager; + private IEnumerable? _filteredItems; + + public MainListPage(IServiceProvider serviceProvider) + { + Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); + _serviceProvider = serviceProvider; + + _tlcManager = _serviceProvider.GetService()!; + _tlcManager.PropertyChanged += TlcManager_PropertyChanged; + _tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged; + + // The all apps page will kick off a BG thread to start loading apps. + // We just want to know when it is done. + var allApps = AllAppsCommandProvider.Page; + allApps.PropChanged += (s, p) => + { + if (p.PropertyName == nameof(allApps.IsLoading)) + { + IsLoading = ActuallyLoading(); + } + }; + + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + var settings = _serviceProvider.GetService()!; + settings.SettingsChanged += SettingsChangedHandler; + HotReloadSettings(settings); + + IsLoading = true; + } + + private void TlcManager_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IsLoading)) + { + IsLoading = ActuallyLoading(); + } + } + + private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count); + + public override IListItem[] GetItems() + { + if (string.IsNullOrEmpty(SearchText)) + { + lock (_tlcManager.TopLevelCommands) + { + return _tlcManager + .TopLevelCommands + .Select(tlc => tlc) + .Where(tlc => !string.IsNullOrEmpty(tlc.Title)) + .ToArray(); + } + } + else + { + lock (_tlcManager.TopLevelCommands) + { + return _filteredItems?.ToArray() ?? []; + } + } + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + // Handle changes to the filter text here + if (!string.IsNullOrEmpty(SearchText)) + { + var aliases = _serviceProvider.GetService()!; + if (aliases.CheckAlias(newSearch)) + { + return; + } + } + + var commands = _tlcManager.TopLevelCommands; + lock (commands) + { + // This gets called on a background thread, because ListViewModel + // updates the .SearchText of all extensions on a BG thread. + foreach (var command in commands) + { + command.TryUpdateFallbackText(newSearch); + } + + // Cleared out the filter text? easy. Reset _filteredItems, and bail out. + if (string.IsNullOrEmpty(newSearch)) + { + _filteredItems = null; + RaiseItemsChanged(commands.Count); + return; + } + + // If the new string doesn't start with the old string, then we can't + // re-use previous results. Reset _filteredItems, and keep er moving. + if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase)) + { + _filteredItems = null; + } + + // If we don't have any previous filter results to work with, start + // with a list of all our commands & apps. + if (_filteredItems == null) + { + IEnumerable apps = AllAppsCommandProvider.Page.GetItems(); + _filteredItems = commands.Concat(apps); + } + + // Produce a list of everything that matches the current filter. + _filteredItems = ListHelpers.FilterList(_filteredItems, SearchText, ScoreTopLevelItem); + RaiseItemsChanged(_filteredItems.Count()); + } + } + + private bool ActuallyLoading() + { + var tlcManager = _serviceProvider.GetService()!; + var allApps = AllAppsCommandProvider.Page; + return allApps.IsLoading || tlcManager.IsLoading; + } + + // 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. + private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem) + { + if (string.IsNullOrWhiteSpace(query)) + { + return 1; + } + + var title = topLevelOrAppItem.Title; + if (string.IsNullOrEmpty(title)) + { + return 0; + } + + var isFallback = false; + var isAliasSubstringMatch = false; + var isAliasMatch = false; + var id = IdForTopLevelOrAppItem(topLevelOrAppItem); + + var extensionDisplayName = string.Empty; + if (topLevelOrAppItem is TopLevelCommandItemWrapper toplevel) + { + isFallback = toplevel.IsFallback; + if (toplevel.Alias?.Alias is string alias) + { + isAliasMatch = alias == query; + isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query, StringComparison.CurrentCultureIgnoreCase); + } + + extensionDisplayName = toplevel.ExtensionHost?.Extension?.PackageDisplayName ?? string.Empty; + } + + var nameMatch = StringMatcher.FuzzySearch(query, title); + var descriptionMatch = StringMatcher.FuzzySearch(query, topLevelOrAppItem.Subtitle); + var extensionTitleMatch = StringMatcher.FuzzySearch(query, extensionDisplayName); + var scores = new[] + { + nameMatch.Score, + (descriptionMatch.Score - 4) / 2.0, + isFallback ? 1 : 0, // Always give fallbacks a chance... + }; + var max = scores.Max(); + max = max + (extensionTitleMatch.Score / 1.5); + + // ... but downweight them + var matchSomething = (max / (isFallback ? 3 : 1)) + + (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0)); + + // If we matched title, subtitle, or alias (something real), then + // here we add the recent command weight boost + // + // Otherwise something like `x` will still match everything you've run before + var finalScore = matchSomething; + if (matchSomething > 0) + { + var history = _serviceProvider.GetService()!.RecentCommands; + var recentWeightBoost = history.GetCommandHistoryWeight(id); + finalScore += recentWeightBoost; + } + + return (int)finalScore; + } + + public void UpdateHistory(IListItem topLevelOrAppItem) + { + var id = IdForTopLevelOrAppItem(topLevelOrAppItem); + var state = _serviceProvider.GetService()!; + var history = state.RecentCommands; + history.AddHistoryItem(id); + AppStateModel.SaveState(state); + } + + private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem) + { + if (topLevelOrAppItem is TopLevelCommandItemWrapper toplevel) + { + return toplevel.Id; + } + else + { + // we've got an app here + return topLevelOrAppItem.Command?.Id ?? string.Empty; + } + } + + public void Receive(ClearSearchMessage message) => SearchText = string.Empty; + + public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count); + + private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender); + + private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs new file mode 100644 index 0000000000..326b7ac2b3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO.Compression; +using System.Text.Json.Nodes; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +internal sealed partial class NewExtensionForm : NewExtensionFormBase +{ + private static readonly string _creatingText = "Creating new extension..."; + private readonly StatusMessage _creatingMessage = new() + { + Message = _creatingText, + Progress = new ProgressState() { IsIndeterminate = true }, + }; + + public NewExtensionForm() + { + TemplateJson = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.6", + "body": [ + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_page_title}}", + "size": "large" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_page_text}}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_name_header}}", + "weight": "bolder", + "size": "default" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_name_description}}", + "wrap": true + }, + { + "type": "Input.Text", + "label": "{{Properties.Resources.builtin_create_extension_name_label}}", + "isRequired": true, + "errorMessage": "{{Properties.Resources.builtin_create_extension_name_required}}", + "id": "ExtensionName", + "placeholder": "ExtensionName", + "regex": "^[^\\s]+$" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_display_name_header}}", + "weight": "bolder", + "size": "default" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_display_name_description}}", + "wrap": true + }, + { + "type": "Input.Text", + "label": "{{Properties.Resources.builtin_create_extension_display_name_label}}", + "isRequired": true, + "errorMessage": "{{Properties.Resources.builtin_create_extension_display_name_required}}", + "id": "DisplayName", + "placeholder": "My new extension" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_directory_header}}", + "weight": "bolder", + "size": "default" + }, + { + "type": "TextBlock", + "text": "{{Properties.Resources.builtin_create_extension_directory_description}}", + "wrap": true + }, + { + "type": "Input.Text", + "label": "{{Properties.Resources.builtin_create_extension_directory_label}}", + "isRequired": true, + "errorMessage": "{{Properties.Resources.builtin_create_extension_directory_required}}", + "id": "OutputPath", + "placeholder": "C:\\users\\me\\dev" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "{{Properties.Resources.builtin_create_extension_submit}}", + "associatedInputs": "auto" + } + ] +} +"""; + } + + public override CommandResult SubmitForm(string payload) + { + var formInput = JsonNode.Parse(payload)?.AsObject(); + if (formInput == null) + { + return CommandResult.KeepOpen(); + } + + var extensionName = formInput["ExtensionName"]?.AsValue()?.ToString() ?? string.Empty; + var displayName = formInput["DisplayName"]?.AsValue()?.ToString() ?? string.Empty; + var outputPath = formInput["OutputPath"]?.AsValue()?.ToString() ?? string.Empty; + + _creatingMessage.State = MessageState.Info; + _creatingMessage.Message = _creatingText; + _creatingMessage.Progress = new ProgressState() { IsIndeterminate = true }; + BuiltinsExtensionHost.Instance.ShowStatus(_creatingMessage, StatusContext.Extension); + + try + { + CreateExtension(extensionName, displayName, outputPath); + + BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage); + + RaiseFormSubmit(new CreatedExtensionForm(extensionName, displayName, outputPath)); + } + catch (Exception e) + { + BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage); + + _creatingMessage.State = MessageState.Error; + _creatingMessage.Message = $"Error: {e.Message}"; + } + + return CommandResult.KeepOpen(); + } + + private void CreateExtension(string extensionName, string newDisplayName, string outputPath) + { + var newGuid = Guid.NewGuid().ToString(); + + // Unzip `template.zip` to a temp dir: + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + // Does the output path exist? + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + var assetsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip"); + ZipFile.ExtractToDirectory(assetsPath, tempDir); + + var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories); + foreach (var file in files) + { + var text = File.ReadAllText(file); + + // Replace all the instances of `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF` with a new random guid: + text = text.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid); + + // Then replace all the `TemplateCmdPalExtension` with `extensionName` + text = text.Replace("TemplateCmdPalExtension", extensionName); + + // Then replace all the `TemplateDisplayName` with `newDisplayName` + text = text.Replace("TemplateDisplayName", newDisplayName); + + // We're going to write the file to the same relative location in the output path + var relativePath = Path.GetRelativePath(tempDir, file); + + var newFileName = Path.Combine(outputPath, relativePath); + + // if the file name had `TemplateCmdPalExtension` in it, replace it with `extensionName` + newFileName = newFileName.Replace("TemplateCmdPalExtension", extensionName); + + // Make sure the directory exists + Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!); + + File.WriteAllText(newFileName, text); + + // Delete the old file + File.Delete(file); + } + + // Delete the temp dir + Directory.Delete(tempDir, true); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionFormBase.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionFormBase.cs new file mode 100644 index 0000000000..e68fef10a4 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionFormBase.cs @@ -0,0 +1,21 @@ +// 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.IO.Compression; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +internal abstract partial class NewExtensionFormBase : FormContent +{ + public event TypedEventHandler? FormSubmitted; + + protected void RaiseFormSubmit(NewExtensionFormBase? next) => FormSubmitted?.Invoke(this, next); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs new file mode 100644 index 0000000000..b8657c1926 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +public partial class NewExtensionPage : ContentPage +{ + private NewExtensionForm _inputForm = new(); + private NewExtensionFormBase? _resultForm; + + public override IContent[] GetContent() + { + return _resultForm != null ? [_resultForm] : [_inputForm]; + } + + public NewExtensionPage() + { + Name = Properties.Resources.builtin_create_extension_name; + Title = Properties.Resources.builtin_create_extension_title; + Icon = new IconInfo("\uEA86"); // Puzzle + + _inputForm.FormSubmitted += FormSubmitted; + } + + private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args) + { + if (_resultForm != null) + { + _resultForm.FormSubmitted -= FormSubmitted; + } + + _resultForm = args; + if (_resultForm != null) + { + _resultForm.FormSubmitted += FormSubmitted; + } + else + { + _inputForm = new(); + _inputForm.FormSubmitted += FormSubmitted; + } + + RaiseItemsChanged(1); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs new file mode 100644 index 0000000000..91d6d3c9e3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs @@ -0,0 +1,25 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +public partial class OpenSettingsCommand : InvokableCommand +{ + public OpenSettingsCommand() + { + Name = Properties.Resources.builtin_open_settings_name; + Icon = new IconInfo("\uE713"); + } + + public override ICommandResult Invoke() + { + WeakReferenceMessenger.Default.Send(); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs new file mode 100644 index 0000000000..d96f46a4a8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +public partial class QuitCommand : InvokableCommand, IFallbackHandler +{ + public QuitCommand() + { + Icon = new IconInfo("\uE711"); + } + + public override ICommandResult Invoke() + { + WeakReferenceMessenger.Default.Send(); + return CommandResult.KeepOpen(); + } + + // this sneaky hidden behavior, I'm not event gonna try to localize this. + public void UpdateQuery(string query) => Name = query.StartsWith('q') ? "Quit" : string.Empty; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs new file mode 100644 index 0000000000..c2fb9f916b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +public partial class ReloadExtensionsCommand : InvokableCommand +{ + public ReloadExtensionsCommand() + { + Icon = new IconInfo("\uE72C"); // Refresh icon + } + + public override ICommandResult Invoke() + { + // 1% BODGY: clear the search before reloading, so that we tell in-proc + // fallback handlers the empty search text + WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(); + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ConfirmResultViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ConfirmResultViewModel.cs new file mode 100644 index 0000000000..53466cb1ce --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ConfirmResultViewModel.cs @@ -0,0 +1,44 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReference context) : + ExtensionObjectViewModel(context) +{ + public ExtensionObject Model { get; } = new(_args); + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public string Title { get; private set; } = string.Empty; + + public string Description { get; private set; } = string.Empty; + + public bool IsPrimaryCommandCritical { get; private set; } + + public CommandViewModel PrimaryCommand { get; private set; } = new(null, context); + + public override void InitializeProperties() + { + var model = Model.Unsafe; + if (model == null) + { + return; + } + + Title = model.Title; + Description = model.Description; + IsPrimaryCommandCritical = model.IsPrimaryCommandCritical; + PrimaryCommand = new(model.PrimaryCommand, PageContext); + PrimaryCommand.InitializeProperties(); + + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(Description)); + UpdateProperty(nameof(IsPrimaryCommandCritical)); + UpdateProperty(nameof(PrimaryCommand)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs new file mode 100644 index 0000000000..e1bbe0b604 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs @@ -0,0 +1,145 @@ +// 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.Text.Json; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Templating; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Windows.Data.Json; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ContentFormViewModel(IFormContent _form, WeakReference context) : + ContentViewModel(context) +{ + private readonly ExtensionObject _formModel = new(_form); + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public string TemplateJson { get; protected set; } = "{}"; + + public string StateJson { get; protected set; } = "{}"; + + public string DataJson { get; protected set; } = "{}"; + + public AdaptiveCardParseResult? Card { get; private set; } + + public override void InitializeProperties() + { + var model = _formModel.Unsafe; + if (model == null) + { + return; + } + + try + { + TemplateJson = model.TemplateJson; + StateJson = model.StateJson; + DataJson = model.DataJson; + + AdaptiveCardTemplate template = new(TemplateJson); + var cardJson = template.Expand(DataJson); + Card = AdaptiveCard.FromJsonString(cardJson); + } + catch (Exception e) + { + // If we fail to parse the card JSON, then display _our own card_ + // with the exception + AdaptiveCardTemplate template = new(ErrorCardJson); + + // todo: we could probably stick Card.Errors in there too + var dataJson = $$""" +{ + "error_message": {{JsonSerializer.Serialize(e.Message)}}, + "error_stack": {{JsonSerializer.Serialize(e.StackTrace)}}, + "inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}}, + "template_json": {{JsonSerializer.Serialize(TemplateJson)}}, + "data_json": {{JsonSerializer.Serialize(DataJson)}} +} +"""; + var cardJson = template.Expand(dataJson); + Card = AdaptiveCard.FromJsonString(cardJson); + } + + UpdateProperty(nameof(Card)); + } + + public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs) + { + if (action is AdaptiveOpenUrlAction openUrlAction) + { + WeakReferenceMessenger.Default.Send(new(openUrlAction.Url)); + return; + } + + if (action is AdaptiveSubmitAction or AdaptiveExecuteAction) + { + // Get the data and inputs + var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty; + var inputString = inputs.Stringify(); + + _ = Task.Run(() => + { + try + { + var model = _formModel.Unsafe!; + if (model != null) + { + var result = model.SubmitForm(inputString, dataString); + WeakReferenceMessenger.Default.Send(new(new(result))); + } + } + catch (Exception ex) + { + ShowException(ex); + } + }); + } + } + + private static readonly string ErrorCardJson = """ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "text": "Error parsing form from extension", + "wrap": true, + "style": "heading", + "size": "ExtraLarge", + "weight": "Bolder", + "color": "Attention" + }, + { + "type": "TextBlock", + "wrap": true, + "text": "${error_message}", + "color": "Attention" + }, + { + "type": "TextBlock", + "text": "${error_stack}", + "fontType": "Monospace" + }, + { + "type": "TextBlock", + "wrap": true, + "text": "Inner exception:" + }, + { + "type": "TextBlock", + "wrap": true, + "text": "${inner_exception}", + "color": "Attention" + } + ] +} +"""; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs new file mode 100644 index 0000000000..c8508e414a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentMarkdownViewModel.cs @@ -0,0 +1,73 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakReference context) : + ContentViewModel(context) +{ + public ExtensionObject Model { get; } = new(_markdown); + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public string Body { get; protected set; } = string.Empty; + + public override void InitializeProperties() + { + var model = Model.Unsafe; + if (model == null) + { + return; + } + + Body = model.Body; + UpdateProperty(nameof(Body)); + + model.PropChanged += Model_PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + var propName = args.PropertyName; + FetchProperty(propName); + } + catch (Exception ex) + { + ShowException(ex); + } + } + + protected void FetchProperty(string propertyName) + { + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Body): + Body = model.Body; + break; + } + + UpdateProperty(propertyName); + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + var model = Model.Unsafe; + if (model != null) + { + model.PropChanged -= Model_PropChanged; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs new file mode 100644 index 0000000000..73bb041b99 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ContentPageViewModel : PageViewModel, ICommandBarContext +{ + private readonly ExtensionObject _model; + + [ObservableProperty] + public partial ObservableCollection Content { get; set; } = []; + + public List Commands { get; private set; } = []; + + public bool HasCommands => Commands.Count > 0; + + public DetailsViewModel? Details { get; private set; } + + [MemberNotNullWhen(true, nameof(Details))] + public bool HasDetails => Details != null; + + /////// ICommandBarContext /////// + public IEnumerable MoreCommands => Commands.Skip(1); + + public bool HasMoreCommands => Commands.Count > 1; + + public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty; + + public CommandItemViewModel? PrimaryCommand => HasCommands ? Commands[0] : null; + + public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? Commands[1] : null; + + public List AllCommands => Commands; + /////// /ICommandBarContext /////// + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, CommandPaletteHost host) + : base(model, scheduler, host) + { + _model = new(model); + } + + // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? + private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent(); + + //// Run on background thread, from InitializeAsync or Model_ItemsChanged + private void FetchContent() + { + List newContent = []; + try + { + var newItems = _model.Unsafe!.GetContent(); + + foreach (var item in newItems) + { + var viewModel = ViewModelFromContent(item, PageContext); + if (viewModel != null) + { + viewModel.InitializeProperties(); + newContent.Add(viewModel); + } + } + } + catch (Exception ex) + { + ShowException(ex, _model?.Unsafe?.Name); + throw; + } + + // Now, back to a UI thread to update the observable collection + DoOnUiThread( + () => + { + ListHelpers.InPlaceUpdateList(Content, newContent); + }); + } + + public static ContentViewModel? ViewModelFromContent(IContent content, WeakReference context) + { + ContentViewModel? viewModel = content switch + { + IFormContent form => new ContentFormViewModel(form, context), + IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context), + ITreeContent tree => new ContentTreeViewModel(tree, context), + _ => null, + }; + return viewModel; + } + + public override void InitializeProperties() + { + base.InitializeProperties(); + + var model = _model.Unsafe; + if (model == null) + { + return; // throw? + } + + Commands = model.Commands + .Where(contextItem => contextItem is ICommandContextItem) + .Select(contextItem => (contextItem as ICommandContextItem)!) + .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) + .ToList(); + Commands.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); + + var extensionDetails = model.Details; + if (extensionDetails != null) + { + Details = new(extensionDetails, PageContext); + Details.InitializeProperties(); + } + + UpdateDetails(); + + FetchContent(); + model.ItemsChanged += Model_ItemsChanged; + + DoOnUiThread( + () => + { + WeakReferenceMessenger.Default.Send(new(this)); + }); + } + + protected override void FetchProperty(string propertyName) + { + base.FetchProperty(propertyName); + + var model = this._model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Commands): + + var more = model.Commands; + if (more != null) + { + var newContextMenu = more + .Where(contextItem => contextItem is ICommandContextItem) + .Select(contextItem => (contextItem as ICommandContextItem)!) + .Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext)) + .ToList(); + lock (Commands) + { + ListHelpers.InPlaceUpdateList(Commands, newContextMenu); + } + + Commands.ForEach(contextItem => + { + contextItem.InitializeProperties(); + }); + } + else + { + Commands.Clear(); + } + + UpdateProperty(nameof(PrimaryCommand)); + UpdateProperty(nameof(SecondaryCommand)); + UpdateProperty(nameof(SecondaryCommandName)); + UpdateProperty(nameof(HasCommands)); + UpdateProperty(nameof(HasMoreCommands)); + UpdateProperty(nameof(AllCommands)); + DoOnUiThread( + () => + { + WeakReferenceMessenger.Default.Send(new(this)); + }); + + break; + case nameof(Details): + var extensionDetails = model.Details; + Details = extensionDetails != null ? new(extensionDetails, PageContext) : null; + UpdateDetails(); + break; + } + + UpdateProperty(propertyName); + } + + private void UpdateDetails() + { + UpdateProperty(nameof(Details)); + UpdateProperty(nameof(HasDetails)); + + DoOnUiThread( + () => + { + if (HasDetails) + { + WeakReferenceMessenger.Default.Send(new(Details)); + } + else + { + WeakReferenceMessenger.Default.Send(); + } + }); + } + + // InvokeItemCommand is what this will be in Xaml due to source generator + // this comes in on Enter keypresses in the SearchBox + [RelayCommand] + private void InvokePrimaryCommand(ContentPageViewModel page) + { + if (PrimaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); + } + } + + // this comes in on Ctrl+Enter keypresses in the SearchBox + [RelayCommand] + private void InvokeSecondaryCommand(ContentPageViewModel page) + { + if (SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); + } + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + Details?.SafeCleanup(); + foreach (var item in Commands) + { + item.SafeCleanup(); + } + + Commands.Clear(); + + foreach (var item in Content) + { + item.SafeCleanup(); + } + + Content.Clear(); + + var model = _model.Unsafe; + if (model != null) + { + model.ItemsChanged -= Model_ItemsChanged; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs new file mode 100644 index 0000000000..77734921aa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference context) : + ContentViewModel(context) +{ + public ExtensionObject Model { get; } = new(_tree); + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public ContentViewModel? RootContent { get; protected set; } + + public ObservableCollection Children { get; } = []; + + public bool HasChildren => Children.Count > 0; + + // This is the content that's actually bound in XAML. We needed a + // collection, even if the collection is just a single item. + public ObservableCollection Root => [RootContent]; + + public override void InitializeProperties() + { + var model = Model.Unsafe; + if (model == null) + { + return; + } + + var root = model.RootContent; + if (root != null) + { + RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext); + RootContent?.InitializeProperties(); + UpdateProperty(nameof(RootContent)); + UpdateProperty(nameof(Root)); + } + + FetchContent(); + model.PropChanged += Model_PropChanged; + model.ItemsChanged += Model_ItemsChanged; + } + + // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? + private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent(); + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + var propName = args.PropertyName; + FetchProperty(propName); + } + catch (Exception ex) + { + ShowException(ex); + } + } + + protected void FetchProperty(string propertyName) + { + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(RootContent): + var root = model.RootContent; + if (root != null) + { + RootContent = ContentPageViewModel.ViewModelFromContent(root, PageContext); + } + else + { + root = null; + } + + UpdateProperty(nameof(Root)); + + break; + } + + UpdateProperty(propertyName); + } + + //// Run on background thread, from InitializeAsync or Model_ItemsChanged + private void FetchContent() + { + List newContent = []; + try + { + var newItems = Model.Unsafe!.GetChildren(); + + foreach (var item in newItems) + { + var viewModel = ContentPageViewModel.ViewModelFromContent(item, PageContext); + if (viewModel != null) + { + viewModel.InitializeProperties(); + newContent.Add(viewModel); + } + } + } + catch (Exception ex) + { + ShowException(ex); + throw; + } + + // Now, back to a UI thread to update the observable collection + DoOnUiThread( + () => + { + ListHelpers.InPlaceUpdateList(Children, newContent); + }); + + UpdateProperty(nameof(HasChildren)); + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + RootContent?.SafeCleanup(); + foreach (var item in Children) + { + item.SafeCleanup(); + } + + Children.Clear(); + var model = Model.Unsafe; + if (model != null) + { + model.PropChanged -= Model_PropChanged; + model.ItemsChanged -= Model_ItemsChanged; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentViewModel.cs new file mode 100644 index 0000000000..9ebf5495fa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentViewModel.cs @@ -0,0 +1,10 @@ +// 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.UI.ViewModels; + +public abstract partial class ContentViewModel(WeakReference context) : + ExtensionObjectViewModel(context) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsDataViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsDataViewModel.cs new file mode 100644 index 0000000000..30084f7c7a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsDataViewModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public abstract partial class DetailsDataViewModel(IPageContext context) : ExtensionObjectViewModel(context) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsElementViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsElementViewModel.cs new file mode 100644 index 0000000000..e836dbf180 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsElementViewModel.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public abstract partial class DetailsElementViewModel(IDetailsElement _detailsElement, WeakReference context) : ExtensionObjectViewModel(context) +{ + private readonly ExtensionObject _model = new(_detailsElement); + + public string Key { get; private set; } = string.Empty; + + public override void InitializeProperties() + { + var model = _model.Unsafe; + if (model == null) + { + return; + } + + Key = model.Key ?? string.Empty; + UpdateProperty(nameof(Key)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsLinkViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsLinkViewModel.cs new file mode 100644 index 0000000000..0d600958da --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsLinkViewModel.cs @@ -0,0 +1,46 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsLinkViewModel( + IDetailsElement _detailsElement, + WeakReference context) : DetailsElementViewModel(_detailsElement, context) +{ + private readonly ExtensionObject _dataModel = + new(_detailsElement.Data as IDetailsLink); + + public string Text { get; private set; } = string.Empty; + + public Uri? Link { get; private set; } + + public bool IsLink => Link != null; + + public bool IsText => !IsLink; + + public override void InitializeProperties() + { + base.InitializeProperties(); + var model = _dataModel.Unsafe; + if (model == null) + { + return; + } + + Text = model.Text ?? string.Empty; + Link = model.Link; + if (string.IsNullOrEmpty(Text) && Link != null) + { + Text = Link.ToString(); + } + + UpdateProperty(nameof(Text)); + UpdateProperty(nameof(Link)); + UpdateProperty(nameof(IsLink)); + UpdateProperty(nameof(IsText)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsSeparatorViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsSeparatorViewModel.cs new file mode 100644 index 0000000000..44b48f0802 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsSeparatorViewModel.cs @@ -0,0 +1,21 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsSeparatorViewModel( + IDetailsElement _detailsElement, + WeakReference context) : DetailsElementViewModel(_detailsElement, context) +{ + private readonly ExtensionObject _dataModel = + new(_detailsElement.Data as IDetailsSeparator); + + public override void InitializeProperties() + { + base.InitializeProperties(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsTagsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsTagsViewModel.cs new file mode 100644 index 0000000000..e188983047 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsTagsViewModel.cs @@ -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.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsTagsViewModel( + IDetailsElement _detailsElement, + WeakReference context) : DetailsElementViewModel(_detailsElement, context) +{ + public List Tags { get; private set; } = []; + + public bool HasTags => Tags.Count > 0; + + private readonly ExtensionObject _dataModel = + new(_detailsElement.Data as IDetailsTags); + + public override void InitializeProperties() + { + base.InitializeProperties(); + var model = _dataModel.Unsafe; + if (model == null) + { + return; + } + + Tags = model + .Tags? + .Select(t => + { + var vm = new TagViewModel(t, PageContext); + vm.InitializeProperties(); + return vm; + }) + .ToList() ?? []; + UpdateProperty(nameof(HasTags)); + UpdateProperty(nameof(Tags)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs new file mode 100644 index 0000000000..0d5233fa41 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs @@ -0,0 +1,63 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsViewModel(IDetails _details, WeakReference context) : ExtensionObjectViewModel(context) +{ + private readonly ExtensionObject _detailsModel = new(_details); + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public IconInfoViewModel HeroImage { get; private set; } = new(null); + + public string Title { get; private set; } = string.Empty; + + public string Body { get; private set; } = string.Empty; + + // Metadata is an array of IDetailsElement, + // where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator} + public List Metadata { get; private set; } = []; + + public override void InitializeProperties() + { + var model = _detailsModel.Unsafe; + if (model == null) + { + return; + } + + Title = model.Title ?? string.Empty; + Body = model.Body ?? string.Empty; + HeroImage = new(model.HeroImage); + HeroImage.InitializeProperties(); + + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(Body)); + UpdateProperty(nameof(HeroImage)); + + var meta = model.Metadata; + if (meta != null) + { + foreach (var element in meta) + { + DetailsElementViewModel? vm = element.Data switch + { + IDetailsSeparator => new DetailsSeparatorViewModel(element, this.PageContext), + IDetailsLink => new DetailsLinkViewModel(element, this.PageContext), + IDetailsTags => new DetailsTagsViewModel(element, this.PageContext), + _ => null, + }; + if (vm != null) + { + vm.InitializeProperties(); + Metadata.Add(vm); + } + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs new file mode 100644 index 0000000000..08d1128106 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionObjectViewModel.cs @@ -0,0 +1,90 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using ManagedCommon; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public abstract partial class ExtensionObjectViewModel : ObservableObject +{ + public WeakReference PageContext { get; set; } + + public ExtensionObjectViewModel(IPageContext? context) + { + var realContext = context ?? (this is IPageContext c ? c : throw new ArgumentException("You need to pass in an IErrorContext")); + PageContext = new(realContext); + } + + public ExtensionObjectViewModel(WeakReference context) + { + PageContext = context; + } + + public async virtual Task InitializePropertiesAsync() + { + var t = new Task(() => + { + SafeInitializePropertiesSynchronous(); + }); + t.Start(); + await t; + } + + public void SafeInitializePropertiesSynchronous() + { + try + { + InitializeProperties(); + } + catch (Exception ex) + { + ShowException(ex); + } + } + + public abstract void InitializeProperties(); + + protected void UpdateProperty(string propertyName) + { + DoOnUiThread(() => OnPropertyChanged(propertyName)); + } + + protected void ShowException(Exception ex, string? extensionHint = null) + { + if (PageContext.TryGetTarget(out var pageContext)) + { + pageContext.ShowException(ex, extensionHint); + } + } + + protected void DoOnUiThread(Action action) + { + if (PageContext.TryGetTarget(out var pageContext)) + { + Task.Factory.StartNew( + action, + CancellationToken.None, + TaskCreationOptions.None, + pageContext.Scheduler); + } + } + + protected virtual void UnsafeCleanup() + { + // base doesn't do anything, but sub-classes should override this. + } + + public virtual void SafeCleanup() + { + try + { + UnsafeCleanup(); + } + catch (Exception ex) + { + Logger.LogDebug(ex.ToString()); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs new file mode 100644 index 0000000000..fde1a36817 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/GlobalLogPageContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class GlobalLogPageContext : IPageContext +{ + public TaskScheduler Scheduler { get; private init; } + + public void ShowException(Exception ex, string? extensionHint) + { /*do nothing*/ + } + + public GlobalLogPageContext() + { + Scheduler = TaskScheduler.FromCurrentSynchronizationContext(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HistoryItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HistoryItem.cs new file mode 100644 index 0000000000..e6ad820cd2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HistoryItem.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public record HistoryItem +{ + public required string CommandId { get; set; } + + public required int Uses { get; set; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HotkeyManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HotkeyManager.cs new file mode 100644 index 0000000000..36498cbcaa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/HotkeyManager.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Settings; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class HotkeyManager : ObservableObject +{ + private readonly TopLevelCommandManager _topLevelCommandManager; + private readonly List _commandHotkeys; + + public HotkeyManager(TopLevelCommandManager tlcManager, SettingsModel settings) + { + _topLevelCommandManager = tlcManager; + _commandHotkeys = settings.CommandHotkeys; + } + + public void UpdateHotkey(string commandId, HotkeySettings? hotkey) + { + // If any of the commands were already bound to this hotkey, remove that + foreach (var item in _commandHotkeys) + { + if (item.Hotkey == hotkey) + { + item.Hotkey = null; + } + } + + _commandHotkeys.RemoveAll(item => item.Hotkey == null); + + foreach (var item in _commandHotkeys) + { + if (item.CommandId == commandId) + { + _commandHotkeys.Remove(item); + break; + } + } + + _commandHotkeys.Add(new(hotkey, commandId)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconDataViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconDataViewModel.cs new file mode 100644 index 0000000000..e969fe5ffd --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconDataViewModel.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class IconDataViewModel : ObservableObject +{ + private readonly ExtensionObject _model = new(null); + + // If the extension previously gave us a Data, then died, the data will + // throw if we actually try to read it, but the pointer itself won't be + // null, so this is relatively safe. + public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null; + + // Locally cached properties from IIconData. + public string Icon { get; private set; } = string.Empty; + + // Streams are not trivially copy-able, so we can't copy the data locally + // first. Hence why we're sticking this into an ExtensionObject + public ExtensionObject Data { get; private set; } = new(null); + + public IconDataViewModel(IIconData? icon) + { + _model = new(icon); + } + + // Unsafe, needs to be called on BG thread + public void InitializeProperties() + { + var model = _model.Unsafe; + if (model == null) + { + return; + } + + Icon = model.Icon; + Data = new(model.Data); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconInfoViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconInfoViewModel.cs new file mode 100644 index 0000000000..df98a4ff8d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IconInfoViewModel.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class IconInfoViewModel : ObservableObject +{ + private readonly ExtensionObject _model = new(null); + + // These are properties that are "observable" from the extension object + // itself, in the sense that they get raised by PropChanged events from the + // extension. However, we don't want to actually make them + // [ObservableProperty]s, because PropChanged comes in off the UI thread, + // and ObservableProperty is not smart enough to raise the PropertyChanged + // on the UI thread. + public IconDataViewModel Light { get; private set; } + + public IconDataViewModel Dark { get; private set; } + + public IconDataViewModel IconForTheme(bool light) => Light = light ? Light : Dark; + + public bool HasIcon(bool light) => IconForTheme(light).HasIcon; + + public bool IsSet => _model.Unsafe != null; + + public IconInfoViewModel(IIconInfo? icon) + { + _model = new(icon); + Light = new(null); + Dark = new(null); + } + + // Unsafe, needs to be called on BG thread + public void InitializeProperties() + { + var model = _model.Unsafe; + if (model == null) + { + return; + } + + Light = new(model.Light); + Light.InitializeProperties(); + + Dark = new(model.Dark); + Dark.InitializeProperties(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs new file mode 100644 index 0000000000..ab7990e781 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs @@ -0,0 +1,146 @@ +// 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 Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ListItemViewModel(IListItem model, WeakReference context) + : CommandItemViewModel(new(model), context) +{ + public new ExtensionObject Model { get; } = new(model); + + public List? Tags { get; set; } + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public bool HasTags => (Tags?.Count ?? 0) > 0; + + public string TextToSuggest { get; private set; } = string.Empty; + + public string Section { get; private set; } = string.Empty; + + public DetailsViewModel? Details { get; private set; } + + [MemberNotNullWhen(true, nameof(Details))] + public bool HasDetails => Details != null; + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + // This sets IsInitialized = true + base.InitializeProperties(); + + var li = Model.Unsafe; + if (li == null) + { + return; // throw? + } + + UpdateTags(li.Tags); + + TextToSuggest = li.TextToSuggest; + Section = li.Section ?? string.Empty; + var extensionDetails = li.Details; + if (extensionDetails != null) + { + Details = new(extensionDetails, PageContext); + Details.InitializeProperties(); + UpdateProperty(nameof(Details)); + UpdateProperty(nameof(HasDetails)); + } + + UpdateProperty(nameof(TextToSuggest)); + UpdateProperty(nameof(Section)); + } + + protected override void FetchProperty(string propertyName) + { + base.FetchProperty(propertyName); + + var model = this.Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Tags): + UpdateTags(model.Tags); + break; + case nameof(TextToSuggest): + this.TextToSuggest = model.TextToSuggest ?? string.Empty; + break; + case nameof(Section): + this.Section = model.Section ?? string.Empty; + break; + case nameof(Details): + var extensionDetails = model.Details; + Details = extensionDetails != null ? new(extensionDetails, PageContext) : null; + Details?.InitializeProperties(); + UpdateProperty(nameof(Details)); + UpdateProperty(nameof(HasDetails)); + break; + } + + UpdateProperty(propertyName); + } + + // TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes? + // TODO: Do we want to save off the score here so we can sort by it in our ListViewModel? + public bool MatchesFilter(string filter) => StringMatcher.FuzzySearch(filter, Title).Success || StringMatcher.FuzzySearch(filter, Subtitle).Success; + + public override string ToString() => $"{Name} ListItemViewModel"; + + public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model); + + public override int GetHashCode() => Model.GetHashCode(); + + private void UpdateTags(ITag[]? newTagsFromModel) + { + DoOnUiThread( + () => + { + var newTags = newTagsFromModel?.Select(t => + { + var vm = new TagViewModel(t, PageContext); + vm.InitializeProperties(); + return vm; + }) + .ToList() ?? []; + + // Tags being an ObservableCollection instead of a List lead to + // many COM exception issues. + Tags = new(newTags); + + UpdateProperty(nameof(Tags)); + UpdateProperty(nameof(HasTags)); + }); + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + // Tags don't have event handlers or anything to cleanup + Tags?.ForEach(t => t.SafeCleanup()); + Details?.SafeCleanup(); + + var model = Model.Unsafe; + if (model != null) + { + // We don't need to revoke the PropChanged event handler here, + // because we are just overriding CommandItem's FetchProperty and + // piggy-backing off their PropChanged + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs new file mode 100644 index 0000000000..f1c6aa4695 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -0,0 +1,498 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ListViewModel : PageViewModel, IDisposable +{ + // private readonly HashSet _itemCache = []; + + // TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items? + + // Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change + // https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support + [ObservableProperty] + public partial ObservableCollection FilteredItems { get; set; } = []; + + private ObservableCollection Items { get; set; } = []; + + private readonly ExtensionObject _model; + + private readonly Lock _listLock = new(); + + private bool _isLoading; + private bool _isFetching; + + public event TypedEventHandler? ItemsUpdated; + + public bool ShowEmptyContent => + IsInitialized && + FilteredItems.Count == 0 && + (!_isFetching) && + IsLoading == false; + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public bool ShowDetails { get; private set; } + + private string _modelPlaceholderText = string.Empty; + + public override string PlaceholderText => _modelPlaceholderText; + + public string SearchText { get; private set; } = string.Empty; + + public CommandItemViewModel EmptyContent { get; private set; } + + private bool _isDynamic; + + private Task? _initializeItemsTask; + private CancellationTokenSource? _cancellationTokenSource; + + public override bool IsInitialized + { + get => base.IsInitialized; protected set + { + base.IsInitialized = value; + UpdateEmptyContent(); + } + } + + public ListViewModel(IListPage model, TaskScheduler scheduler, CommandPaletteHost host) + : base(model, scheduler, host) + { + _model = new(model); + EmptyContent = new(new(null), PageContext); + } + + // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? + private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems(); + + protected override void OnFilterUpdated(string filter) + { + //// TODO: Just temp testing, need to think about where we want to filter, as ACVS in View could be done, but then grouping need CVS, maybe we do grouping in view + //// and manage filtering below, but we should be smarter about this and understand caching and other requirements... + //// Investigate if we re-use src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\ListHelpers.cs InPlaceUpdateList and FilterList? + + // Dynamic pages will handler their own filtering. They will tell us if + // something needs to change, by raising ItemsChanged. + if (_isDynamic) + { + // We're getting called on the UI thread. + // Hop off to a BG thread to update the extension. + _ = Task.Run(() => + { + try + { + if (_model.Unsafe is IDynamicListPage dynamic) + { + dynamic.SearchText = filter; + } + } + catch (Exception ex) + { + ShowException(ex, _model?.Unsafe?.Name); + } + }); + } + else + { + // But for all normal pages, we should run our fuzzy match on them. + lock (_listLock) + { + ApplyFilterUnderLock(); + } + + ItemsUpdated?.Invoke(this, EventArgs.Empty); + UpdateEmptyContent(); + _isLoading = false; + } + } + + //// Run on background thread, from InitializeAsync or Model_ItemsChanged + private void FetchItems() + { + // TEMPORARY: just plop all the items into a single group + // see 9806fe5d8 for the last commit that had this with sections + _isFetching = true; + + try + { + IListItem[] newItems = _model.Unsafe!.GetItems(); + + // Collect all the items into new viewmodels + Collection newViewModels = []; + + // TODO we can probably further optimize this by also keeping a + // HashSet of every ExtensionObject we currently have, and only + // building new viewmodels for the ones we haven't already built. + foreach (IListItem? item in newItems) + { + ListItemViewModel viewModel = new(item, new(this)); + + // If an item fails to load, silently ignore it. + if (viewModel.SafeFastInit()) + { + newViewModels.Add(viewModel); + } + } + + IEnumerable firstTwenty = newViewModels.Take(20); + foreach (ListItemViewModel? item in firstTwenty) + { + item?.SafeInitializeProperties(); + } + + // Cancel any ongoing search + if (_cancellationTokenSource != null) + { + _cancellationTokenSource.Cancel(); + } + + lock (_listLock) + { + // Now that we have new ViewModels for everything from the + // extension, smartly update our list of VMs + ListHelpers.InPlaceUpdateList(Items, newViewModels); + } + + // TODO: Iterate over everything in Items, and prune items from the + // cache if we don't need them anymore + } + catch (Exception ex) + { + // TODO: Move this within the for loop, so we can catch issues with individual items + // Create a special ListItemViewModel for errors and use an ItemTemplateSelector in the ListPage to display error items differently. + ShowException(ex, _model?.Unsafe?.Name); + throw; + } + finally + { + _isFetching = false; + } + + _cancellationTokenSource = new CancellationTokenSource(); + + _initializeItemsTask = new Task(() => + { + try + { + InitializeItemsTask(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + } + }); + _initializeItemsTask.Start(); + + DoOnUiThread( + () => + { + lock (_listLock) + { + // Now that our Items contains everything we want, it's time for us to + // re-evaluate our Filter on those items. + if (!_isDynamic) + { + // A static list? Great! Just run the filter. + ApplyFilterUnderLock(); + } + else + { + // A dynamic list? Even better! Just stick everything into + // FilteredItems. The extension already did any filtering it cared about. + ListHelpers.InPlaceUpdateList(FilteredItems, Items.Where(i => !i.IsInErrorState)); + } + + UpdateEmptyContent(); + } + + ItemsUpdated?.Invoke(this, EventArgs.Empty); + _isLoading = false; + }); + } + + private void InitializeItemsTask(CancellationToken ct) + { + // Were we already canceled? + ct.ThrowIfCancellationRequested(); + + ListItemViewModel[] iterable; + lock (_listLock) + { + iterable = Items.ToArray(); + } + + foreach (ListItemViewModel item in iterable) + { + ct.ThrowIfCancellationRequested(); + + // TODO: GH #502 + // We should probably remove the item from the list if it + // entered the error state. I had issues doing that without having + // multiple threads muck with `Items` (and possibly FilteredItems!) + // at once. + item.SafeInitializeProperties(); + + ct.ThrowIfCancellationRequested(); + } + } + + /// + /// Apply our current filter text to the list of items, and update + /// FilteredItems to match the results. + /// + private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter)); + + /// + /// Helper to generate a weighting for a given list item, based on title, + /// subtitle, etc. Largely a copy of the version in ListHelpers, but + /// operating on ViewModels instead of extension objects. + /// + private static int ScoreListItem(string query, CommandItemViewModel listItem) + { + if (string.IsNullOrEmpty(query)) + { + return 1; + } + + MatchResult nameMatch = StringMatcher.FuzzySearch(query, listItem.Title); + MatchResult descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle); + return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max(); + } + + private struct ScoredListItemViewModel + { + public int Score; + public ListItemViewModel ViewModel; + } + + // Similarly stolen from ListHelpers.FilterList + public static IEnumerable FilterList(IEnumerable items, string query) + { + IOrderedEnumerable scores = items + .Where(i => !i.IsInErrorState) + .Select(li => new ScoredListItemViewModel() { ViewModel = li, Score = ScoreListItem(query, li) }) + .Where(score => score.Score > 0) + .OrderByDescending(score => score.Score); + return scores + .Select(score => score.ViewModel); + } + + // InvokeItemCommand is what this will be in Xaml due to source generator + // This is what gets invoked when the user presses + [RelayCommand] + private void InvokeItem(ListItemViewModel? item) + { + if (item != null) + { + WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); + } + else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null) + { + WeakReferenceMessenger.Default.Send(new( + EmptyContent.PrimaryCommand.Command.Model, + EmptyContent.PrimaryCommand.Model)); + } + } + + // This is what gets invoked when the user presses + [RelayCommand] + private void InvokeSecondaryCommand(ListItemViewModel? item) + { + if (item != null) + { + if (item.SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(item.SecondaryCommand.Command.Model, item.Model)); + } + } + else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null) + { + WeakReferenceMessenger.Default.Send(new( + EmptyContent.SecondaryCommand.Command.Model, + EmptyContent.SecondaryCommand.Model)); + } + } + + [RelayCommand] + private void UpdateSelectedItem(ListItemViewModel item) + { + if (!item.SafeSlowInit()) + { + return; + } + + // GH #322: + // For inexplicable reasons, if you try updating the command bar and + // the details on the same UI thread tick as updating the list, we'll + // explode + DoOnUiThread( + () => + { + WeakReferenceMessenger.Default.Send(new(item)); + + if (ShowDetails && item.HasDetails) + { + WeakReferenceMessenger.Default.Send(new(item.Details)); + } + else + { + WeakReferenceMessenger.Default.Send(); + } + + TextToSuggest = item.TextToSuggest; + }); + } + + public override void InitializeProperties() + { + base.InitializeProperties(); + + IListPage? model = _model.Unsafe; + if (model == null) + { + return; // throw? + } + + _isDynamic = model is IDynamicListPage; + + ShowDetails = model.ShowDetails; + UpdateProperty(nameof(ShowDetails)); + + _modelPlaceholderText = model.PlaceholderText; + UpdateProperty(nameof(PlaceholderText)); + + SearchText = model.SearchText; + UpdateProperty(nameof(SearchText)); + + EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent.SlowInitializeProperties(); + + FetchItems(); + model.ItemsChanged += Model_ItemsChanged; + } + + public void LoadMoreIfNeeded() + { + IListPage? model = this._model.Unsafe; + if (model == null) + { + return; + } + + if (model.HasMoreItems && !_isLoading) + { + _isLoading = true; + _ = Task.Run(() => + { + try + { + model.LoadMore(); + } + catch (Exception ex) + { + ShowException(ex, model.Name); + } + }); + } + } + + protected override void FetchProperty(string propertyName) + { + base.FetchProperty(propertyName); + + IListPage? model = this._model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(ShowDetails): + this.ShowDetails = model.ShowDetails; + break; + case nameof(PlaceholderText): + this._modelPlaceholderText = model.PlaceholderText; + break; + case nameof(SearchText): + this.SearchText = model.SearchText; + break; + case nameof(EmptyContent): + EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent.InitializeProperties(); + break; + case nameof(IsLoading): + UpdateEmptyContent(); + break; + } + + UpdateProperty(propertyName); + } + + private void UpdateEmptyContent() + { + UpdateProperty(nameof(ShowEmptyContent)); + if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null) + { + return; + } + + DoOnUiThread( + () => + { + WeakReferenceMessenger.Default.Send(new(EmptyContent)); + }); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + EmptyContent?.SafeCleanup(); + EmptyContent = new(new(null), PageContext); // necessary? + + _cancellationTokenSource?.Cancel(); + + lock (_listLock) + { + foreach (ListItemViewModel item in Items) + { + item.SafeCleanup(); + } + + Items.Clear(); + foreach (ListItemViewModel item in FilteredItems) + { + item.SafeCleanup(); + } + + FilteredItems.Clear(); + } + + IListPage? model = _model.Unsafe; + if (model != null) + { + model.ItemsChanged -= Model_ItemsChanged; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs new file mode 100644 index 0000000000..60571cbe73 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LoadingPageViewModel.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class LoadingPageViewModel : PageViewModel +{ + public LoadingPageViewModel(IPage? model, TaskScheduler scheduler) + : base(model, scheduler, CommandPaletteHost.Instance) + { + ModelIsLoading = true; + IsInitialized = false; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs new file mode 100644 index 0000000000..60d065ac04 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/LogMessageViewModel.cs @@ -0,0 +1,34 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class LogMessageViewModel : ExtensionObjectViewModel +{ + private readonly ExtensionObject _model; + + public string Message { get; private set; } = string.Empty; + + public string ExtensionPfn { get; set; } = string.Empty; + + public LogMessageViewModel(ILogMessage message, IPageContext context) + : base(context) + { + _model = new(message); + } + + public override void InitializeProperties() + { + var model = _model.Unsafe; + if (model == null) + { + return; // throw? + } + + Message = model.Message; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSecondaryCommandMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSecondaryCommandMessage.cs new file mode 100644 index 0000000000..1e35d6d796 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSecondaryCommandMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to perform a list item's secondary command when the user presses ctrl+enter in the search box +/// +public record ActivateSecondaryCommandMessage +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSelectedListItemMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSelectedListItemMessage.cs new file mode 100644 index 0000000000..339e0c1b3d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ActivateSelectedListItemMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to perform a list item's command when the user presses enter in the search box +/// +public record ActivateSelectedListItemMessage +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ClearSearchMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ClearSearchMessage.cs new file mode 100644 index 0000000000..e3b7ee831e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ClearSearchMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record ClearSearchMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/DismissMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/DismissMessage.cs new file mode 100644 index 0000000000..273952897d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/DismissMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record DismissMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/FocusSearchBoxMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/FocusSearchBoxMessage.cs new file mode 100644 index 0000000000..4e6db37daa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/FocusSearchBoxMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record FocusSearchBoxMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs new file mode 100644 index 0000000000..2200fe2c56 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/GoHomeMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record GoHomeMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HandleCommandResultMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HandleCommandResultMessage.cs new file mode 100644 index 0000000000..3adf7ae30c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HandleCommandResultMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record HandleCommandResultMessage(ExtensionObject Result) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HideDetailsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HideDetailsMessage.cs new file mode 100644 index 0000000000..a04455b519 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HideDetailsMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record HideDetailsMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HotkeySummonMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HotkeySummonMessage.cs new file mode 100644 index 0000000000..bff7af364e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/HotkeySummonMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record HotkeySummonMessage(string CommandId, IntPtr Hwnd) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/LaunchUriMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/LaunchUriMessage.cs new file mode 100644 index 0000000000..dba45af1b7 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/LaunchUriMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record LaunchUriMessage(Uri Uri) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateBackMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateBackMessage.cs new file mode 100644 index 0000000000..5e19bfc57f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateBackMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record NavigateBackMessage(bool FromBackspace = false) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateNextCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateNextCommand.cs new file mode 100644 index 0000000000..c996e0eecf --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigateNextCommand.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to navigate to the next command in the page when pressing the Down key in the SearchBox. +/// +public record NavigateNextCommand +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigatePreviousCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigatePreviousCommand.cs new file mode 100644 index 0000000000..76f0f07908 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/NavigatePreviousCommand.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to navigate to the previous command in the page when pressing the Down key in the SearchBox. +/// +public record NavigatePreviousCommand +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenContextMenuMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenContextMenuMessage.cs new file mode 100644 index 0000000000..c35ab284af --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenContextMenuMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to perform a list item's secondary command when the user presses ctrl+enter in the search box +/// +public record OpenContextMenuMessage +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs new file mode 100644 index 0000000000..115593d5ae --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs @@ -0,0 +1,11 @@ +// 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.UI.ViewModels.BuiltinCommands; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record OpenSettingsMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs new file mode 100644 index 0000000000..c45809cfaa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs @@ -0,0 +1,56 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to do a command - navigate to a page or invoke it +/// +public record PerformCommandMessage +{ + public ExtensionObject Command { get; } + + public object? Context { get; } + + public bool WithAnimation { get; set; } = true; + + public PerformCommandMessage(ExtensionObject command) + { + Command = command; + Context = null; + } + + public PerformCommandMessage(TopLevelCommandItemWrapper topLevelCommand) + { + Command = new(topLevelCommand.Command); + Context = null; + } + + public PerformCommandMessage(ExtensionObject command, ExtensionObject context) + { + Command = command; + Context = context.Unsafe; + } + + public PerformCommandMessage(ExtensionObject command, ExtensionObject context) + { + Command = command; + Context = context.Unsafe; + } + + public PerformCommandMessage(ExtensionObject command, ExtensionObject context) + { + Command = command; + Context = context.Unsafe; + } + + public PerformCommandMessage(ConfirmResultViewModel vm) + { + Command = vm.PrimaryCommand.Model; + Context = null; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs new file mode 100644 index 0000000000..2951aa57fb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Message which closes the application. Used by via . +/// +public record QuitMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs new file mode 100644 index 0000000000..c427da4f9c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record ReloadCommandsMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowDetailsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowDetailsMessage.cs new file mode 100644 index 0000000000..b51300d6d0 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowDetailsMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record ShowDetailsMessage(DetailsViewModel Details) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowWindowMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowWindowMessage.cs new file mode 100644 index 0000000000..9e880c08f0 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ShowWindowMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record ShowWindowMessage(IntPtr Hwnd) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs new file mode 100644 index 0000000000..0a540c7408 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +/// +/// Used to update the command bar at the bottom to reflect the commands for a list item +/// +public record UpdateCommandBarMessage(ICommandBarContext? ViewModel) +{ +} + +// Represents everything the command bar needs to know about to show command +// buttons at the bottom. +// +// This is implemented by both ListItemViewModel and ContentPageViewModel, +// the two things with sub-commands. +public interface ICommandBarContext : INotifyPropertyChanged +{ + public IEnumerable MoreCommands { get; } + + public bool HasMoreCommands { get; } + + public string SecondaryCommandName { get; } + + public CommandItemViewModel? PrimaryCommand { get; } + + public CommandItemViewModel? SecondaryCommand { get; } + + public List AllCommands { get; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs new file mode 100644 index 0000000000..c7503ce1fb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs @@ -0,0 +1,9 @@ +// 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.UI.ViewModels.Messages; + +public record UpdateFallbackItemsMessage() +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj new file mode 100644 index 0000000000..819d6e1e1b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj @@ -0,0 +1,58 @@ + + + + enable + enable + false + false + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + + preview + + SA1313; + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionObject`1.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionObject`1.cs new file mode 100644 index 0000000000..822229addc --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionObject`1.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Models; + +public class ExtensionObject(T? value) // where T : IInspectable +{ + public T? Unsafe { get; } = value; + + public override bool Equals(object? obj) => obj is ExtensionObject ext && ext.Unsafe?.Equals(this.Unsafe) == true; + + public override int GetHashCode() => Unsafe?.GetHashCode() ?? base.GetHashCode(); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs new file mode 100644 index 0000000000..232f35c3fa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Common.Services; +using Microsoft.CommandPalette.Extensions; +using Windows.ApplicationModel; +using Windows.ApplicationModel.AppExtensions; +using Windows.Foundation; +using Windows.Foundation.Collections; + +namespace Microsoft.CmdPal.UI.ViewModels.Models; + +public class ExtensionService : IExtensionService, IDisposable +{ + public event TypedEventHandler>? OnExtensionAdded; + + public event TypedEventHandler>? OnExtensionRemoved; + + private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser(); + private static readonly Lock _lock = new(); + private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1); + private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1); + + // private readonly ILocalSettingsService _localSettingsService; + private bool _disposedValue; + + private const string CreateInstanceProperty = "CreateInstance"; + private const string ClassIdProperty = "@ClassId"; + + private static readonly List _installedExtensions = []; + private static readonly List _enabledExtensions = []; + + public ExtensionService() + { + _catalog.PackageInstalling += Catalog_PackageInstalling; + _catalog.PackageUninstalling += Catalog_PackageUninstalling; + _catalog.PackageUpdating += Catalog_PackageUpdating; + + //// These two were an investigation into getting updates when a package + //// gets redeployed from VS. Neither get raised (nor do the above) + //// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged; + //// _catalog.PackageStaging += Catalog_PackageStaging; + // _localSettingsService = settingsService; + } + + private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args) + { + if (args.IsComplete) + { + lock (_lock) + { + InstallPackageUnderLock(args.Package); + } + } + } + + private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args) + { + if (args.IsComplete) + { + lock (_lock) + { + UninstallPackageUnderLock(args.Package); + } + } + } + + private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args) + { + if (args.IsComplete) + { + lock (_lock) + { + // Get any extension providers that we previously had from this app + UninstallPackageUnderLock(args.TargetPackage); + + // then add the new ones. + InstallPackageUnderLock(args.TargetPackage); + } + } + } + + private void InstallPackageUnderLock(Package package) + { + var isCmdPalExtensionResult = Task.Run(() => + { + return IsValidCmdPalExtension(package); + }).Result; + var isExtension = isCmdPalExtensionResult.IsExtension; + var extension = isCmdPalExtensionResult.Extension; + if (isExtension && extension != null) + { + CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}"); + + Task.Run(async () => + { + await _getInstalledExtensionsLock.WaitAsync(); + try + { + var wrappers = await CreateWrappersForExtension(extension); + + UpdateExtensionsListsFromWrappers(wrappers); + + OnExtensionAdded?.Invoke(this, wrappers); + } + finally + { + _getInstalledExtensionsLock.Release(); + } + }); + } + } + + private void UninstallPackageUnderLock(Package package) + { + List removedExtensions = []; + foreach (var extension in _installedExtensions) + { + if (extension.PackageFullName == package.Id.FullName) + { + CommandPaletteHost.Instance.DebugLog($"Uninstalled extension app {extension.PackageDisplayName}"); + + removedExtensions.Add(extension); + } + } + + Task.Run(async () => + { + await _getInstalledExtensionsLock.WaitAsync(); + try + { + _installedExtensions.RemoveAll(i => removedExtensions.Contains(i)); + + OnExtensionRemoved?.Invoke(this, removedExtensions); + } + finally + { + _getInstalledExtensionsLock.Release(); + } + }); + } + + private static async Task IsValidCmdPalExtension(Package package) + { + var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync(); + foreach (var extension in extensions) + { + if (package.Id?.FullName == extension.Package?.Id?.FullName) + { + var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension); + + return new(cmdPalProvider != null && classId.Count != 0, extension); + } + } + + return new(false, null); + } + + private static async Task<(IPropertySet? CmdPalProvider, List ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension) + { + var classIds = new List(); + var properties = await extension.GetExtensionPropertiesAsync(); + + if (properties is null) + { + return (null, classIds); + } + + var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider"); + if (cmdPalProvider is null) + { + return (null, classIds); + } + + var activation = GetSubPropertySet(cmdPalProvider, "Activation"); + if (activation is null) + { + return (cmdPalProvider, classIds); + } + + // Handle case where extension creates multiple instances. + classIds.AddRange(GetCreateInstanceList(activation)); + + return (cmdPalProvider, classIds); + } + + private static async Task> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync(); + + public async Task> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false) + { + await _getInstalledExtensionsLock.WaitAsync(); + try + { + if (_installedExtensions.Count == 0) + { + var extensions = await GetInstalledAppExtensionsAsync(); + foreach (var extension in extensions) + { + var wrappers = await CreateWrappersForExtension(extension); + UpdateExtensionsListsFromWrappers(wrappers); + } + } + + return includeDisabledExtensions ? _installedExtensions : _enabledExtensions; + } + finally + { + _getInstalledExtensionsLock.Release(); + } + } + + private static void UpdateExtensionsListsFromWrappers(List wrappers) + { + foreach (var extensionWrapper in wrappers) + { + // var localSettingsService = Application.Current.GetService(); + var extensionUniqueId = extensionWrapper.ExtensionUniqueId; + var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync(extensionUniqueId + "-ExtensionDisabled"); + + _installedExtensions.Add(extensionWrapper); + if (!isExtensionDisabled) + { + _enabledExtensions.Add(extensionWrapper); + } + + // TelemetryFactory.Get().Log( + // "Extension_ReportInstalled", + // LogLevel.Critical, + // new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled)); + } + } + + private static async Task> CreateWrappersForExtension(AppExtension extension) + { + var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension); + + if (cmdPalProvider == null || classIds.Count == 0) + { + return []; + } + + List wrappers = []; + foreach (var classId in classIds) + { + var extensionWrapper = CreateExtensionWrapper(extension, cmdPalProvider, classId); + wrappers.Add(extensionWrapper); + } + + return wrappers; + } + + private static ExtensionWrapper CreateExtensionWrapper(AppExtension extension, IPropertySet cmdPalProvider, string classId) + { + var extensionWrapper = new ExtensionWrapper(extension, classId); + + var supportedInterfaces = GetSubPropertySet(cmdPalProvider, "SupportedInterfaces"); + if (supportedInterfaces is not null) + { + foreach (var supportedInterface in supportedInterfaces) + { + ProviderType pt; + if (Enum.TryParse(supportedInterface.Key, out pt)) + { + extensionWrapper.AddProviderType(pt); + } + else + { + // log warning that extension declared unsupported extension interface + CommandPaletteHost.Instance.DebugLog($"Extension {extension.DisplayName} declared an unsupported interface: {supportedInterface.Key}"); + } + } + } + + return extensionWrapper; + } + + public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId) + { + var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); + return extension.FirstOrDefault(); + } + + public async Task SignalStopExtensionsAsync() + { + var installedExtensions = await GetInstalledExtensionsAsync(); + foreach (var installedExtension in installedExtensions) + { + if (installedExtension.IsRunning()) + { + installedExtension.SignalDispose(); + } + } + } + + public async Task> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false) + { + var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions); + + List filteredExtensions = []; + foreach (var installedExtension in installedExtensions) + { + if (installedExtension.HasProviderType(providerType)) + { + filteredExtensions.Add(installedExtension); + } + } + + return filteredExtensions; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _getInstalledExtensionsLock.Dispose(); + _getInstalledWidgetsLock.Dispose(); + } + + _disposedValue = true; + } + } + + private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null; + + private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null; + + /// + /// There are cases where the extension creates multiple COM instances. + /// + /// Activation property set object + /// List of ClassId strings associated with the activation property + private static List GetCreateInstanceList(IPropertySet activationPropSet) + { + var propSetList = new List(); + var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); + if (singlePropertySet != null) + { + var classId = GetProperty(singlePropertySet, ClassIdProperty); + + // If the instance has a classId as a single string, then it's only supporting a single instance. + if (classId != null) + { + propSetList.Add(classId); + } + } + else + { + var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); + if (propertySetArray != null) + { + foreach (var prop in propertySetArray) + { + if (prop is not IPropertySet propertySet) + { + continue; + } + + var classId = GetProperty(propertySet, ClassIdProperty); + if (classId != null) + { + propSetList.Add(classId); + } + } + } + } + + return propSetList; + } + + private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string; + + public void EnableExtension(string extensionUniqueId) + { + var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); + _enabledExtensions.Add(extension.First()); + } + + public void DisableExtension(string extensionUniqueId) + { + var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); + _enabledExtensions.Remove(extension.First()); + } + + /* + ///// + //public async Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension) + //{ + // // Only attempt to disable feature if its available. + // if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId)) + // { + // return false; + // } + // _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown"); + // // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension + // // for the rest of its process lifetime. + // DisableExtension(extension.ExtensionUniqueId); + // // Update the local settings so the next time the user launches Dev Home the extension will be disabled. + // await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true); + // return true; + //} */ +} + +internal record struct IsExtensionResult(bool IsExtension, AppExtension? Extension) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs new file mode 100644 index 0000000000..7ff4230a0c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CommandPalette.Extensions; +using Windows.ApplicationModel; +using Windows.ApplicationModel.AppExtensions; +using Windows.Win32; +using Windows.Win32.System.Com; +using WinRT; + +namespace Microsoft.CmdPal.UI.ViewModels.Models; + +public class ExtensionWrapper : IExtensionWrapper +{ + private const int HResultRpcServerNotRunning = -2147023174; + + private readonly string _appUserModelId; + private readonly string _extensionId; + + private readonly Lock _lock = new(); + private readonly List _providerTypes = []; + + private readonly Dictionary _providerTypeMap = new() + { + [typeof(ICommandProvider)] = ProviderType.Commands, + }; + + private IExtension? _extensionObject; + + public ExtensionWrapper(AppExtension appExtension, string classId) + { + PackageDisplayName = appExtension.Package.DisplayName; + ExtensionDisplayName = appExtension.DisplayName; + PackageFullName = appExtension.Package.Id.FullName; + PackageFamilyName = appExtension.Package.Id.FamilyName; + ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId)); + Publisher = appExtension.Package.PublisherDisplayName; + InstalledDate = appExtension.Package.InstalledDate; + Version = appExtension.Package.Id.Version; + _appUserModelId = appExtension.AppInfo.AppUserModelId; + _extensionId = appExtension.Id; + } + + public string PackageDisplayName { get; } + + public string ExtensionDisplayName { get; } + + public string PackageFullName { get; } + + public string PackageFamilyName { get; } + + public string ExtensionClassId { get; } + + public string Publisher { get; } + + public DateTimeOffset InstalledDate { get; } + + public PackageVersion Version { get; } + + /// + /// Gets the unique id for this Dev Home extension. The unique id is a concatenation of: + /// + /// The AppUserModelId (AUMID) of the extension's application. The AUMID is the concatenation of the package + /// family name and the application id and uniquely identifies the application containing the extension within + /// the package. + /// The Extension Id. This is the unique identifier of the extension within the application. + /// + /// + public string ExtensionUniqueId => _appUserModelId + "!" + _extensionId; + + public bool IsRunning() + { + if (_extensionObject is null) + { + return false; + } + + try + { + _extensionObject.As().GetRuntimeClassName(); + } + catch (COMException e) + { + if (e.ErrorCode == HResultRpcServerNotRunning) + { + return false; + } + + throw; + } + + return true; + } + + public async Task StartExtensionAsync() + { + await Task.Run(() => + { + lock (_lock) + { + if (!IsRunning()) + { + Logger.LogDebug($"Starting {ExtensionDisplayName} ({ExtensionClassId})"); + + nint extensionPtr = nint.Zero; + try + { + // -2147024809: E_INVALIDARG + // -2147467262: E_NOINTERFACE + // -2147024893: E_PATH_NOT_FOUND + Guid guid = typeof(IExtension).GUID; + global::Windows.Win32.Foundation.HRESULT hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out object? extensionObj); + + if (hr.Value == -2147024893) + { + Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted."); + + // We don't really need to throw this exception. + // We'll just return out nothing. + return; + } + + extensionPtr = Marshal.GetIUnknownForObject(extensionObj); + if (hr < 0) + { + Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}"); + Marshal.ThrowExceptionForHR(hr); + } + + _extensionObject = MarshalInterface.FromAbi(extensionPtr); + } + finally + { + if (extensionPtr != nint.Zero) + { + Marshal.Release(extensionPtr); + } + } + } + } + }); + } + + public void SignalDispose() + { + lock (_lock) + { + if (IsRunning()) + { + _extensionObject?.Dispose(); + } + + _extensionObject = null; + } + } + + public IExtension? GetExtensionObject() + { + lock (_lock) + { + return IsRunning() ? _extensionObject : null; + } + } + + public async Task GetProviderAsync() + where T : class + { + await StartExtensionAsync(); + + return GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]) as T; + } + + public async Task> GetListOfProvidersAsync() + where T : class + { + await StartExtensionAsync(); + + object? supportedProviders = GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]); + if (supportedProviders is IEnumerable multipleProvidersSupported) + { + return multipleProvidersSupported; + } + else if (supportedProviders is T singleProviderSupported) + { + return [singleProviderSupported]; + } + + return Enumerable.Empty(); + } + + public void AddProviderType(ProviderType providerType) => _providerTypes.Add(providerType); + + public bool HasProviderType(ProviderType providerType) => _providerTypes.Contains(providerType); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.txt new file mode 100644 index 0000000000..981c7446f7 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.txt @@ -0,0 +1,19 @@ +GetPhysicallyInstalledSystemMemory +GlobalMemoryStatusEx +GetSystemInfo +CoCreateInstance +SetForegroundWindow +IsIconic +RegisterHotKey +SetWindowLongPtr +CallWindowProc +ShowWindow +SetForegroundWindow +SetFocus +SetActiveWindow +MonitorFromWindow +GetMonitorInfo +SHCreateStreamOnFileEx +CoAllowSetForegroundWindow +SHCreateStreamOnFileEx +SHLoadIndirectString diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs new file mode 100644 index 0000000000..a082f0acd2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class PageViewModel : ExtensionObjectViewModel, IPageContext +{ + public TaskScheduler Scheduler { get; private set; } + + private readonly ExtensionObject _pageModel; + + public bool IsLoading => ModelIsLoading || (!IsInitialized); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsLoading))] + public virtual partial bool IsInitialized { get; protected set; } + + [ObservableProperty] + public partial string ErrorMessage { get; protected set; } = string.Empty; + + [ObservableProperty] + public partial bool IsNested { get; set; } = true; + + // This is set from the SearchBar + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowSuggestion))] + public partial string Filter { get; set; } = string.Empty; + + [ObservableProperty] + public virtual partial string PlaceholderText { get; private set; } = "Type here to search..."; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowSuggestion))] + public virtual partial string TextToSuggest { get; protected set; } = string.Empty; + + public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != Filter; + + [ObservableProperty] + public partial CommandPaletteHost ExtensionHost { get; private set; } + + public bool HasStatusMessage => MostRecentStatusMessage != null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasStatusMessage))] + public partial StatusMessageViewModel? MostRecentStatusMessage { get; private set; } = null; + + public ObservableCollection StatusMessages => ExtensionHost.StatusMessages; + + // These are properties that are "observable" from the extension object + // itself, in the sense that they get raised by PropChanged events from the + // extension. However, we don't want to actually make them + // [ObservableProperty]s, because PropChanged comes in off the UI thread, + // and ObservableProperty is not smart enough to raise the PropertyChanged + // on the UI thread. + public string Name { get; protected set; } = string.Empty; + + public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty; + + // This property maps to `IPage.IsLoading`, but we want to expose our own + // `IsLoading` property as a combo of this value and `IsInitialized` + public bool ModelIsLoading { get; protected set; } = true; + + public IconInfoViewModel Icon { get; protected set; } + + public PageViewModel(IPage? model, TaskScheduler scheduler, CommandPaletteHost extensionHost) + : base((IPageContext?)null) + { + _pageModel = new(model); + Scheduler = scheduler; + PageContext = new(this); + ExtensionHost = extensionHost; + Icon = new(null); + + ExtensionHost.StatusMessages.CollectionChanged += StatusMessages_CollectionChanged; + UpdateHasStatusMessage(); + } + + private void StatusMessages_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) => UpdateHasStatusMessage(); + + private void UpdateHasStatusMessage() + { + if (ExtensionHost.StatusMessages.Any()) + { + var last = ExtensionHost.StatusMessages.Last(); + MostRecentStatusMessage = last; + } + else + { + MostRecentStatusMessage = null; + } + } + + //// Run on background thread from ListPage.xaml.cs + [RelayCommand] + private Task InitializeAsync() + { + // TODO: We may want a SemaphoreSlim lock here. + + // TODO: We may want to investigate using some sort of AsyncEnumerable or populating these as they come into the UI layer + // Though we have to think about threading here and circling back to the UI thread with a TaskScheduler. + try + { + InitializeProperties(); + } + catch (Exception ex) + { + ShowException(ex, _pageModel?.Unsafe?.Name); + return Task.FromResult(false); + } + + // Notify we're done back on the UI Thread. + Task.Factory.StartNew( + () => + { + IsInitialized = true; + + // TODO: Do we want an event/signal here that the Page Views can listen to? (i.e. ListPage setting the selected index to 0, however, in async world the user may have already started navigating around page...) + }, + CancellationToken.None, + TaskCreationOptions.None, + Scheduler); + return Task.FromResult(true); + } + + public override void InitializeProperties() + { + var page = _pageModel.Unsafe; + if (page == null) + { + return; // throw? + } + + Name = page.Name; + ModelIsLoading = page.IsLoading; + Title = page.Title; + Icon = new(page.Icon); + Icon.InitializeProperties(); + + // Let the UI know about our initial properties too. + UpdateProperty(nameof(Name)); + UpdateProperty(nameof(Title)); + UpdateProperty(nameof(ModelIsLoading)); + UpdateProperty(nameof(IsLoading)); + UpdateProperty(nameof(Icon)); + + page.PropChanged += Model_PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + var propName = args.PropertyName; + FetchProperty(propName); + } + catch (Exception ex) + { + ShowException(ex, _pageModel?.Unsafe?.Name); + } + } + + partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue); + + protected virtual void OnFilterUpdated(string filter) + { + // The base page has no notion of data, so we do nothing here... + // subclasses should override. + } + + protected virtual void FetchProperty(string propertyName) + { + var model = this._pageModel.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Name): + this.Name = model.Name ?? string.Empty; + UpdateProperty(nameof(Title)); + break; + case nameof(Title): + this.Title = model.Title ?? string.Empty; + break; + case nameof(IsLoading): + this.ModelIsLoading = model.IsLoading; + UpdateProperty(nameof(ModelIsLoading)); + break; + case nameof(Icon): + this.Icon = new(model.Icon); + break; + } + + UpdateProperty(propertyName); + } + + public new void ShowException(Exception ex, string? extensionHint = null) + { + // Set the extensionHint to the Page Title (if we have one, and one not provided). + // extensionHint ??= _pageModel?.Unsafe?.Title; + extensionHint ??= ExtensionHost.Extension?.ExtensionDisplayName ?? Title; + Task.Factory.StartNew( + () => + { + ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n"; + }, + CancellationToken.None, + TaskCreationOptions.None, + Scheduler); + } + + public override string ToString() => $"{Title} ViewModel"; + + protected override void UnsafeCleanup() + { + base.UnsafeCleanup(); + + ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged; + + var model = _pageModel.Unsafe; + if (model != null) + { + model.PropChanged -= Model_PropChanged; + } + } +} + +public interface IPageContext +{ + public void ShowException(Exception ex, string? extensionHint = null); + + public TaskScheduler Scheduler { get; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProgressViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProgressViewModel.cs new file mode 100644 index 0000000000..3a2d4512ed --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProgressViewModel.cs @@ -0,0 +1,70 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ProgressViewModel : ExtensionObjectViewModel +{ + public ExtensionObject Model { get; } + + public bool IsIndeterminate { get; private set; } + + public uint ProgressPercent { get; private set; } + + public ProgressViewModel(IProgressState progress, WeakReference context) + : base(context) + { + Model = new(progress); + } + + public override void InitializeProperties() + { + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + IsIndeterminate = model.IsIndeterminate; + ProgressPercent = model.ProgressPercent; + + model.PropChanged += Model_PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + FetchProperty(args.PropertyName); + } + catch (Exception ex) + { + ShowException(ex); + } + } + + protected virtual void FetchProperty(string propertyName) + { + var model = this.Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(IsIndeterminate): + this.IsIndeterminate = model.IsIndeterminate; + break; + case nameof(ProgressPercent): + this.ProgressPercent = model.ProgressPercent; + break; + } + + UpdateProperty(propertyName); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..7b65624363 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs @@ -0,0 +1,414 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.UI.ViewModels.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.UI.ViewModels.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Create another. + /// + public static string builtin_create_extension_create_another { + get { + return ResourceManager.GetString("builtin_create_extension_create_another", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Where should the new extension be created? This path will be created if it doesn't exist. + /// + public static string builtin_create_extension_directory_description { + get { + return ResourceManager.GetString("builtin_create_extension_directory_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output path. + /// + public static string builtin_create_extension_directory_header { + get { + return ResourceManager.GetString("builtin_create_extension_directory_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output path. + /// + public static string builtin_create_extension_directory_label { + get { + return ResourceManager.GetString("builtin_create_extension_directory_label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output path is required. + /// + public static string builtin_create_extension_directory_required { + get { + return ResourceManager.GetString("builtin_create_extension_directory_required", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of your extension as users will see it.. + /// + public static string builtin_create_extension_display_name_description { + get { + return ResourceManager.GetString("builtin_create_extension_display_name_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display name. + /// + public static string builtin_create_extension_display_name_header { + get { + return ResourceManager.GetString("builtin_create_extension_display_name_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display name. + /// + public static string builtin_create_extension_display_name_label { + get { + return ResourceManager.GetString("builtin_create_extension_display_name_label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display name is required. + /// + public static string builtin_create_extension_display_name_required { + get { + return ResourceManager.GetString("builtin_create_extension_display_name_required", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string builtin_create_extension_name { + get { + return ResourceManager.GetString("builtin_create_extension_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is the name of your new extension project. It should be a valid C# class name. Best practice is to also include the word 'Extension' in the name.. + /// + public static string builtin_create_extension_name_description { + get { + return ResourceManager.GetString("builtin_create_extension_name_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extension name. + /// + public static string builtin_create_extension_name_header { + get { + return ResourceManager.GetString("builtin_create_extension_name_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extension name. + /// + public static string builtin_create_extension_name_label { + get { + return ResourceManager.GetString("builtin_create_extension_name_label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extension name is required, without spaces. + /// + public static string builtin_create_extension_name_required { + get { + return ResourceManager.GetString("builtin_create_extension_name_required", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open directory. + /// + public static string builtin_create_extension_open_directory { + get { + return ResourceManager.GetString("builtin_create_extension_open_directory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Solution. + /// + public static string builtin_create_extension_open_solution { + get { + return ResourceManager.GetString("builtin_create_extension_open_solution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use this page to create a new extension project.. + /// + public static string builtin_create_extension_page_text { + get { + return ResourceManager.GetString("builtin_create_extension_page_text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create your new extension. + /// + public static string builtin_create_extension_page_title { + get { + return ResourceManager.GetString("builtin_create_extension_page_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create extension. + /// + public static string builtin_create_extension_submit { + get { + return ResourceManager.GetString("builtin_create_extension_submit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully created your new extension!. + /// + public static string builtin_create_extension_success { + get { + return ResourceManager.GetString("builtin_create_extension_success", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create a new extension. + /// + public static string builtin_create_extension_title { + get { + return ResourceManager.GetString("builtin_create_extension_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your new extension '${displayName}' was created in:. + /// + public static string builtin_created_in_text { + get { + return ResourceManager.GetString("builtin_created_in_text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Now that your extension project has been created, open the solution up in Visual Studio to start writing your extension code.. + /// + public static string builtin_created_next_steps { + get { + return ResourceManager.GetString("builtin_created_next_steps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navigate to `${name}Page.cs` to start adding items to the list, or to `${name}CommandsProvider.cs` to add new commands.. + /// + public static string builtin_created_next_steps_p2 { + get { + return ResourceManager.GetString("builtin_created_next_steps_p2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Once you're ready to test deploy the package locally with Visual Studio, then run the \"Reload\" command in the Command Palette to load your new extension.. + /// + public static string builtin_created_next_steps_p3 { + get { + return ResourceManager.GetString("builtin_created_next_steps_p3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next steps. + /// + public static string builtin_created_next_steps_title { + get { + return ResourceManager.GetString("builtin_created_next_steps_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating new extension.... + /// + public static string builtin_creating_extension_message { + get { + return ResourceManager.GetString("builtin_creating_extension_message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Built-in commands. + /// + public static string builtin_display_name { + get { + return ResourceManager.GetString("builtin_display_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View log. + /// + public static string builtin_log_name { + get { + return ResourceManager.GetString("builtin_log_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log. + /// + public static string builtin_log_page_name { + get { + return ResourceManager.GetString("builtin_log_page_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View log messages. + /// + public static string builtin_log_subtitle { + get { + return ResourceManager.GetString("builtin_log_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View log. + /// + public static string builtin_log_title { + get { + return ResourceManager.GetString("builtin_log_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creates a project for a new Command Palette extension. + /// + public static string builtin_new_extension_subtitle { + get { + return ResourceManager.GetString("builtin_new_extension_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Settings. + /// + public static string builtin_open_settings_name { + get { + return ResourceManager.GetString("builtin_open_settings_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Command Palette settings. + /// + public static string builtin_open_settings_subtitle { + get { + return ResourceManager.GetString("builtin_open_settings_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit Command Palette. + /// + public static string builtin_quit_subtitle { + get { + return ResourceManager.GetString("builtin_quit_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reload Command Palette extensions. + /// + public static string builtin_reload_display_title { + get { + return ResourceManager.GetString("builtin_reload_display_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reload. + /// + public static string builtin_reload_name { + get { + return ResourceManager.GetString("builtin_reload_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reload Command Palette extensions. + /// + public static string builtin_reload_subtitle { + get { + return ResourceManager.GetString("builtin_reload_subtitle", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx new file mode 100644 index 0000000000..063eda3d30 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Open Command Palette settings + + + Creates a project for a new Command Palette extension + + + Exit Command Palette + + + Built-in commands + + + View log messages + + + View log + + + Reload Command Palette extensions + + + Reload + + + View log + + + Log + + + Creating new extension... + + + Open + + + Create a new extension + + + Open Settings + + + Successfully created your new extension! + + + Your new extension '${displayName}' was created in: + {Locked="'${displayName}'"} + + + Next steps + + + Now that your extension project has been created, open the solution up in Visual Studio to start writing your extension code. + + + Navigate to `${name}Page.cs` to start adding items to the list, or to `${name}CommandsProvider.cs` to add new commands. + {Locked="`${name}Page.cs`", "`${name}CommandsProvider.cs`"} + + + Once you're ready to test deploy the package locally with Visual Studio, then run the \"Reload\" command in the Command Palette to load your new extension. + + + Create your new extension + + + Use this page to create a new extension project. + + + Extension name + + + This is the name of your new extension project. It should be a valid C# class name. Best practice is to also include the word 'Extension' in the name. + + + Extension name + + + Extension name is required, without spaces + + + Display name + + + The name of your extension as users will see it. + + + Display name + + + Display name is required + + + Output path + + + Where should the new extension be created? This path will be created if it doesn't exist + + + Output path + + + Output path is required + + + Open Solution + + + Open directory + + + Create another + + + Create extension + + + Reload Command Palette extensions + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs new file mode 100644 index 0000000000..ad857f669e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class ProviderSettings +{ + public bool IsEnabled { get; set; } = true; + + [JsonIgnore] + public string PackageFamilyName { get; set; } = string.Empty; + + [JsonIgnore] + public string Id { get; set; } = string.Empty; + + [JsonIgnore] + public string ProviderDisplayName { get; set; } = string.Empty; + + // Originally, I wanted to do: + // public string ProviderId => $"{PackageFamilyName}/{ProviderDisplayName}"; + // but I think that's actually a bad idea, because the Display Name can be localized. + [JsonIgnore] + public string ProviderId => $"{PackageFamilyName}/{Id}"; + + [JsonIgnore] + public bool IsBuiltin => string.IsNullOrEmpty(PackageFamilyName); + + public ProviderSettings(CommandProviderWrapper wrapper) + { + Connect(wrapper); + } + + [JsonConstructor] + public ProviderSettings(bool isEnabled) + { + IsEnabled = isEnabled; + } + + public void Connect(CommandProviderWrapper wrapper) + { + PackageFamilyName = wrapper.Extension?.PackageFamilyName ?? string.Empty; + Id = wrapper.DisplayName; + ProviderDisplayName = wrapper.DisplayName; + if (ProviderId == "/") + { + throw new InvalidDataException("Did you add a built-in command and forget to set the Id? Make sure you do that!"); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs new file mode 100644 index 0000000000..79fe6638c5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -0,0 +1,81 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.Common.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ProviderSettingsViewModel( + CommandProviderWrapper _provider, + ProviderSettings _providerSettings, + IServiceProvider _serviceProvider) : ObservableObject +{ + private readonly TopLevelCommandManager _tlcManager = _serviceProvider.GetService()!; + private readonly SettingsModel _settings = _serviceProvider.GetService()!; + + public string DisplayName => _provider.DisplayName; + + public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? "Built-in"; + + public string ExtensionSubtext => $"{ExtensionName}, {TopLevelCommands.Count} commands"; + + [MemberNotNullWhen(true, nameof(Extension))] + public bool IsFromExtension => _provider.Extension != null; + + public IExtensionWrapper? Extension => _provider.Extension; + + public string ExtensionVersion => IsFromExtension ? $"{Extension.Version.Major}.{Extension.Version.Minor}.{Extension.Version.Build}.{Extension.Version.Revision}" : string.Empty; + + public IconInfoViewModel Icon => _provider.Icon; + + public bool IsEnabled + { + get => _providerSettings.IsEnabled; + set => _providerSettings.IsEnabled = value; + } + + public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null; + + public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null; + + [field: AllowNull] + public List TopLevelCommands + { + get + { + if (field == null) + { + field = BuildTopLevelViewModels(); + } + + return field; + } + } + + private List BuildTopLevelViewModels() + { + var topLevelCommands = _tlcManager.TopLevelCommands; + var thisProvider = _provider; + var providersCommands = thisProvider.TopLevelItems; + List results = []; + + // Remember! This comes in on the UI thread! + // TODO: GH #426 + // Probably just do a background InitializeProperties + // Or better yet, merge TopLevelCommandWrapper and TopLevelViewModel + foreach (var command in providersCommands) + { + var match = topLevelCommands.Where(tlc => tlc.Model.Unsafe == command).FirstOrDefault(); + if (match != null) + { + results.Add(new(match, _settings, _serviceProvider)); + } + } + + return results; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs new file mode 100644 index 0000000000..9e971ae510 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs @@ -0,0 +1,74 @@ +// 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.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class RecentCommandsManager : ObservableObject +{ + [JsonInclude] + private List History { get; set; } = []; + + public RecentCommandsManager() + { + } + + public int GetCommandHistoryWeight(string commandId) + { + var entry = History + .Index() + .Where(item => item.Item.CommandId == commandId) + .FirstOrDefault(); + + // These numbers are vaguely scaled so that "VS" will make "Visual Studio" the + // match after one use. + // Usually it has a weight of 84, compared to 109 for the VS cmd prompt + if (entry.Item != null) + { + var index = entry.Index; + + // First, add some weight based on how early in the list this appears + var bucket = index switch + { + var i when index <= 2 => 35, + var i when index <= 10 => 25, + var i when index <= 15 => 15, + var i when index <= 35 => 10, + _ => 5, + }; + + // Then, add weight for how often this is used, but cap the weight from usage. + var uses = Math.Min(entry.Item.Uses * 5, 35); + + return bucket + uses; + } + + return 0; + } + + public void AddHistoryItem(string commandId) + { + var entry = History + .Where(item => item.CommandId == commandId) + .FirstOrDefault(); + if (entry == null) + { + var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 }; + History.Insert(0, newitem); + } + else + { + History.Remove(entry); + entry.Uses++; + History.Insert(0, entry); + } + + if (History.Count > 50) + { + History.RemoveRange(50, History.Count - 50); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/Helper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/Helper.cs new file mode 100644 index 0000000000..b135d9006d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/Helper.cs @@ -0,0 +1,22 @@ +// 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.UI.ViewModels.Settings; + +public static class Helper +{ + private static readonly global::PowerToys.Interop.LayoutMapManaged LayoutMap = new(); + + public static string GetKeyName(uint key) + { + return LayoutMap.GetKeyName(key); + } + + public static uint GetKeyValue(string key) + { + return LayoutMap.GetKeyValue(key); + } + + public static readonly uint VirtualKeyWindows = global::PowerToys.Interop.Constants.VK_WIN_BOTH; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/HotkeySettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/HotkeySettings.cs new file mode 100644 index 0000000000..c3c3a59e65 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Settings/HotkeySettings.cs @@ -0,0 +1,230 @@ +// 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 System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels.Settings; + +public record HotkeySettings// : ICmdLineRepresentable +{ + private const int VKTAB = 0x09; + + public HotkeySettings() + { + Win = false; + Ctrl = false; + Alt = false; + Shift = false; + Code = 0; + } + + /// + /// Initializes a new instance of the class. + /// + /// Should Windows key be used + /// Should Ctrl key be used + /// Should Alt key be used + /// Should Shift key be used + /// Go to https://learn.microsoft.com/windows/win32/inputdev/virtual-key-codes to see list of v-keys + public HotkeySettings(bool win, bool ctrl, bool alt, bool shift, int code) + { + Win = win; + Ctrl = ctrl; + Alt = alt; + Shift = shift; + Code = code; + } + + [JsonPropertyName("win")] + public bool Win { get; set; } + + [JsonPropertyName("ctrl")] + public bool Ctrl { get; set; } + + [JsonPropertyName("alt")] + public bool Alt { get; set; } + + [JsonPropertyName("shift")] + public bool Shift { get; set; } + + [JsonPropertyName("code")] + public int Code { get; set; } + + // This is currently needed for FancyZones, we need to unify these two objects + // see src\common\settings_objects.h + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + public override string ToString() + { + var output = new StringBuilder(); + + if (Win) + { + output.Append("Win + "); + } + + if (Ctrl) + { + output.Append("Ctrl + "); + } + + if (Alt) + { + output.Append("Alt + "); + } + + if (Shift) + { + output.Append("Shift + "); + } + + if (Code > 0) + { + var localKey = Helper.GetKeyName((uint)Code); + output.Append(localKey); + } + else if (output.Length >= 2) + { + output.Remove(output.Length - 2, 2); + } + + return output.ToString(); + } + + public List GetKeysList() + { + var shortcutList = new List(); + + if (Win) + { + shortcutList.Add(92); // The Windows key or button. + } + + if (Ctrl) + { + shortcutList.Add("Ctrl"); + } + + if (Alt) + { + shortcutList.Add("Alt"); + } + + if (Shift) + { + shortcutList.Add("Shift"); + + // shortcutList.Add(16); // The Shift key or button. + } + + if (Code > 0) + { + switch (Code) + { + // https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348 + case 38: // The Up Arrow key or button. + case 40: // The Down Arrow key or button. + case 37: // The Left Arrow key or button. + case 39: // The Right Arrow key or button. + // case 8: // The Back key or button. + // case 13: // The Enter key or button. + shortcutList.Add(Code); + break; + default: + var localKey = Helper.GetKeyName((uint)Code); + shortcutList.Add(localKey); + break; + } + } + + return shortcutList; + } + + public bool IsValid() + { + return IsAccessibleShortcut() ? false : (Alt || Ctrl || Win || Shift) && Code != 0; + } + + public bool IsEmpty() + { + return !Alt && !Ctrl && !Win && !Shift && Code == 0; + } + + public bool IsAccessibleShortcut() + { + // Shift+Tab and Tab are accessible shortcuts + return (!Alt && !Ctrl && !Win && Shift && Code == VKTAB) + || (!Alt && !Ctrl && !Win && !Shift && Code == VKTAB); + } + + public static bool TryParseFromCmd(string cmd, out object? result) + { + bool win = false, ctrl = false, alt = false, shift = false; + var code = 0; + + var parts = cmd.Split('+'); + foreach (var part in parts) + { + switch (part.Trim().ToLower(CultureInfo.InvariantCulture)) + { + case "win": + win = true; + break; + case "ctrl": + ctrl = true; + break; + case "alt": + alt = true; + break; + case "shift": + shift = true; + break; + default: + if (!TryParseKeyCode(part, out code)) + { + result = null; + return false; + } + + break; + } + } + + result = new HotkeySettings(win, ctrl, alt, shift, code); + return true; + } + + private static bool TryParseKeyCode(string key, out int keyCode) + { + // ASCII symbol + if (key.Length == 1 && char.IsLetterOrDigit(key[0])) + { + keyCode = char.ToUpper(key[0], CultureInfo.InvariantCulture); + return true; + } + + // VK code + else if (key.Length == 4 && key.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return int.TryParse(key.AsSpan(2), NumberStyles.HexNumber, null, out keyCode); + } + + // Alias + else + { + keyCode = (int)Helper.GetKeyValue(key); + return keyCode != 0; + } + } + + public bool TryToCmdRepresentable(out string result) + { + result = ToString(); + result = result.Replace(" ", null); + return true; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs new file mode 100644 index 0000000000..60e87459eb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -0,0 +1,165 @@ +// 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; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Settings; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class SettingsModel : ObservableObject +{ + [JsonIgnore] + public static readonly string FilePath; + + public event TypedEventHandler? SettingsChanged; + + /////////////////////////////////////////////////////////////////////////// + // SETTINGS HERE + public HotkeySettings? Hotkey { get; set; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space + + public bool ShowAppDetails { get; set; } + + public bool HotkeyGoesHome { get; set; } + + public bool BackspaceGoesBack { get; set; } + + public bool SingleClickActivates { get; set; } + + public bool HighlightSearchOnActivate { get; set; } = true; + + public Dictionary ProviderSettings { get; set; } = []; + + public Dictionary Aliases { get; set; } = []; + + public List CommandHotkeys { get; set; } = []; + + public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse; + + // END SETTINGS + /////////////////////////////////////////////////////////////////////////// + + static SettingsModel() + { + FilePath = SettingsJsonPath(); + } + + public static SettingsModel LoadSettings() + { + if (string.IsNullOrEmpty(FilePath)) + { + throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadSettings)}"); + } + + if (!File.Exists(FilePath)) + { + Debug.WriteLine("The provided settings file does not exist"); + return new(); + } + + try + { + // Read the JSON content from the file + var jsonContent = File.ReadAllText(FilePath); + + var loaded = JsonSerializer.Deserialize(jsonContent, _deserializerOptions); + + Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + + return loaded ?? new(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + + return new(); + } + + public static void SaveSettings(SettingsModel model) + { + if (string.IsNullOrEmpty(FilePath)) + { + throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveSettings)}"); + } + + try + { + // Serialize the main dictionary to JSON and save it to the file + var settingsJson = JsonSerializer.Serialize(model, _serializerOptions); + + // Is it valid JSON? + if (JsonNode.Parse(settingsJson) is JsonObject newSettings) + { + // Now, read the existing content from the file + var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}"; + + // Is it valid JSON? + if (JsonNode.Parse(oldContent) is JsonObject savedSettings) + { + foreach (var item in newSettings) + { + savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null; + } + + var serialized = savedSettings.ToJsonString(_serializerOptions); + File.WriteAllText(FilePath, serialized); + + // TODO: Instead of just raising the event here, we should + // have a file change watcher on the settings file, and + // reload the settings then + model.SettingsChanged?.Invoke(model, null); + } + else + { + Debug.WriteLine("Failed to parse settings file as JsonObject."); + } + } + else + { + Debug.WriteLine("Failed to parse settings file as JsonObject."); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + } + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the settings is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + private static readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() }, + }; + + private static readonly JsonSerializerOptions _deserializerOptions = new() + { + PropertyNameCaseInsensitive = true, + IncludeFields = true, + Converters = { new JsonStringEnumConverter() }, + AllowTrailingCommas = true, + }; +} + +public enum MonitorBehavior +{ + ToMouse = 0, + ToPrimary = 1, + ToFocusedWindow = 2, + InPlace = 3, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000000..104404083f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Microsoft.CmdPal.UI.ViewModels.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class SettingsViewModel +{ + private readonly SettingsModel _settings; + private readonly IServiceProvider _serviceProvider; + + public HotkeySettings? Hotkey + { + get => _settings.Hotkey; + set + { + _settings.Hotkey = value; + Save(); + } + } + + public bool ShowAppDetails + { + get => _settings.ShowAppDetails; + set + { + _settings.ShowAppDetails = value; + Save(); + } + } + + public bool HotkeyGoesHome + { + get => _settings.HotkeyGoesHome; + set + { + _settings.HotkeyGoesHome = value; + Save(); + } + } + + public bool BackspaceGoesBack + { + get => _settings.BackspaceGoesBack; + set + { + _settings.BackspaceGoesBack = value; + Save(); + } + } + + public bool SingleClickActivates + { + get => _settings.SingleClickActivates; + set + { + _settings.SingleClickActivates = value; + Save(); + } + } + + public bool HighlightSearchOnActivate + { + get => _settings.HighlightSearchOnActivate; + set + { + _settings.HighlightSearchOnActivate = value; + Save(); + } + } + + public int MonitorPositionIndex + { + get => (int)_settings.SummonOn; + set + { + _settings.SummonOn = (MonitorBehavior)value; + Save(); + } + } + + public ObservableCollection CommandProviders { get; } = []; + + public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler) + { + _settings = settings; + _serviceProvider = serviceProvider; + + var activeProviders = GetCommandProviders(); + var allProviderSettings = _settings.ProviderSettings; + + foreach (var item in activeProviders) + { + if (!allProviderSettings.TryGetValue(item.ProviderId, out var value)) + { + allProviderSettings[item.ProviderId] = new ProviderSettings(item); + } + else + { + value.Connect(item); + } + + var providerSettings = allProviderSettings.TryGetValue(item.ProviderId, out var value2) ? + value2 : + new ProviderSettings(item); + + var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider); + CommandProviders.Add(settingsModel); + } + } + + private IEnumerable GetCommandProviders() + { + var manager = _serviceProvider.GetService()!; + var allProviders = manager.CommandProviders; + return allProviders; + } + + private void Save() => SettingsModel.SaveSettings(_settings); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs new file mode 100644 index 0000000000..9d1a90af55 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs @@ -0,0 +1,177 @@ +// 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 CommunityToolkit.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.MainPage; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Windows.Win32; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskScheduler _scheduler) : ObservableObject +{ + [ObservableProperty] + public partial bool IsLoaded { get; set; } = false; + + [ObservableProperty] + public partial DetailsViewModel? Details { get; set; } + + [ObservableProperty] + public partial bool IsDetailsVisible { get; set; } + + [ObservableProperty] + public partial PageViewModel CurrentPage { get; set; } = new LoadingPageViewModel(null, _scheduler); + + private MainListPage? _mainListPage; + + private IExtensionWrapper? _activeExtension; + + [RelayCommand] + public async Task LoadAsync() + { + var tlcManager = _serviceProvider.GetService(); + await tlcManager!.LoadBuiltinsAsync(); + IsLoaded = true; + + // Built-ins have loaded. We can display our page at this point. + _mainListPage = new MainListPage(_serviceProvider); + WeakReferenceMessenger.Default.Send(new(new ExtensionObject(_mainListPage))); + + _ = Task.Run(async () => + { + // After loading built-ins, and starting navigation, kick off a thread to load extensions. + tlcManager.LoadExtensionsCommand.Execute(null); + + await tlcManager.LoadExtensionsCommand.ExecutionTask!; + if (tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion) + { + // TODO: Handle failure case + } + }); + + return true; + } + + public void LoadPageViewModel(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 + // This triggers that load generally with the InitializeCommand asynchronously when we navigate to a page. + // We could re-track the page loading status, if we need it more granularly below again, but between the initialized and error message, we can infer some status. + // We could also maybe move this thread offloading we do for loading into PageViewModel and better communicate between the two... a few different options. + + ////LoadedState = ViewModelLoadedState.Loading; + if (!viewModel.IsInitialized + && viewModel.InitializeCommand != null) + { + _ = 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) + { + // TODO: Handle failure case + if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex) + { + Logger.LogError(ex.ToString()); + } + + // TODO GH #239 switch back when using the new MD text block + // _ = _queue.EnqueueAsync(() => + /*_queue.TryEnqueue(new(() => + { + LoadedState = ViewModelLoadedState.Error; + }));*/ + } + else + { + // TODO GH #239 switch back when using the new MD text block + // _ = _queue.EnqueueAsync(() => + _ = Task.Factory.StartNew( + () => + { + var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!; + + CurrentPage = viewModel; // result ? viewModel : null; + ////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error; + }, + CancellationToken.None, + TaskCreationOptions.None, + _scheduler); + } + }); + } + else + { + CurrentPage = viewModel; + ////LoadedState = ViewModelLoadedState.Loaded; + } + } + + public void PerformTopLevelCommand(PerformCommandMessage message) + { + if (_mainListPage == null) + { + return; + } + + if (message.Context is IListItem listItem) + { + _mainListPage.UpdateHistory(listItem); + } + } + + public void SetActiveExtension(IExtensionWrapper? extension) + { + if (extension != _activeExtension) + { + // There's not really a CoDisallowSetForegroundWindow, so we don't + // need to handle that + _activeExtension = extension; + + var extensionComObject = _activeExtension?.GetExtensionObject(); + if (extensionComObject != null) + { + try + { + unsafe + { + var hr = PInvoke.CoAllowSetForegroundWindow(extensionComObject); + if (hr != 0) + { + Logger.LogWarning($"Error giving foreground rights: 0x{hr.Value:X8}"); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex.ToString()); + } + } + } + } + + public void GoHome() + { + SetActiveExtension(null); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs new file mode 100644 index 0000000000..32328b0eb1 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/StatusMessageViewModel.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class StatusMessageViewModel : ExtensionObjectViewModel +{ + public ExtensionObject Model { get; } + + public string Message { get; private set; } = string.Empty; + + public MessageState State { get; private set; } = MessageState.Info; + + public string ExtensionPfn { get; set; } = string.Empty; + + public ProgressViewModel? Progress { get; private set; } + + public bool HasProgress => Progress != null; + + // public bool IsIndeterminate => Progress != null && Progress.IsIndeterminate; + // public double ProgressValue => (Progress?.ProgressPercent ?? 0) / 100.0; + public StatusMessageViewModel(IStatusMessage message, WeakReference context) + : base(context) + { + Model = new(message); + } + + public override void InitializeProperties() + { + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + Message = model.Message; + State = model.State; + var modelProgress = model.Progress; + if (modelProgress != null) + { + Progress = new(modelProgress, this.PageContext); + Progress.InitializeProperties(); + UpdateProperty(nameof(HasProgress)); + } + + model.PropChanged += Model_PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + FetchProperty(args.PropertyName); + } + catch (Exception ex) + { + ShowException(ex); + } + } + + protected virtual void FetchProperty(string propertyName) + { + var model = this.Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Message): + this.Message = model.Message; + break; + case nameof(State): + this.State = model.State; + break; + case nameof(Progress): + var modelProgress = model.Progress; + if (modelProgress != null) + { + Progress = new(modelProgress, this.PageContext); + Progress.InitializeProperties(); + } + else + { + Progress = null; + } + + UpdateProperty(nameof(HasProgress)); + break; + } + + UpdateProperty(propertyName); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs new file mode 100644 index 0000000000..414f1882a5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs @@ -0,0 +1,49 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class TagViewModel(ITag _tag, WeakReference context) : ExtensionObjectViewModel(context) +{ + private readonly ExtensionObject _tagModel = new(_tag); + + public string ToolTip => string.IsNullOrEmpty(ModelToolTip) ? Text : ModelToolTip; + + // Remember - "observable" properties from the model (via PropChanged) + // cannot be marked [ObservableProperty] + public string Text { get; private set; } = string.Empty; + + public string ModelToolTip { get; private set; } = string.Empty; + + public OptionalColor Foreground { get; private set; } + + public OptionalColor Background { get; private set; } + + public IconInfoViewModel Icon { get; private set; } = new(null); + + public override void InitializeProperties() + { + var model = _tagModel.Unsafe; + if (model == null) + { + return; + } + + Text = model.Text; + Foreground = model.Foreground; + Background = model.Background; + ModelToolTip = model.ToolTip; + Icon = new(model.Icon); + Icon.InitializeProperties(); + + UpdateProperty(nameof(Text)); + UpdateProperty(nameof(Foreground)); + UpdateProperty(nameof(Background)); + UpdateProperty(nameof(ToolTip)); + UpdateProperty(nameof(Icon)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ToastViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ToastViewModel.cs new file mode 100644 index 0000000000..156a47f557 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ToastViewModel.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class ToastViewModel : ObservableObject +{ + [ObservableProperty] + public partial string ToastMessage { get; set; } = string.Empty; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs new file mode 100644 index 0000000000..a6cb0489cf --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs @@ -0,0 +1,224 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CmdPal.UI.ViewModels.Settings; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Extensions.DependencyInjection; +using WyHash; + +namespace Microsoft.CmdPal.UI.ViewModels; + +/// +/// Abstraction of a top-level command. Currently owns just a live ICommandItem +/// from an extension (or in-proc command provider), but in the future will +/// also support stub top-level items. +/// +public partial class TopLevelCommandItemWrapper : ListItem +{ + private readonly IServiceProvider _serviceProvider; + private readonly string _commandProviderId; + + public ExtensionObject Model { get; } + + public bool IsFallback { get; private set; } + + private readonly string _idFromModel = string.Empty; + private string _generatedId = string.Empty; + + public string Id => string.IsNullOrEmpty(_idFromModel) ? _generatedId : _idFromModel; + + private readonly TopLevelCommandWrapper _topLevelCommand; + + public CommandAlias? Alias { get; private set; } + + private HotkeySettings? _hotkey; + + public HotkeySettings? Hotkey + { + get => _hotkey; + set + { + UpdateHotkey(); + UpdateTags(); + } + } + + public CommandPaletteHost ExtensionHost { get => _topLevelCommand.ExtensionHost; } + + public TopLevelCommandItemWrapper( + ExtensionObject commandItem, + bool isFallback, + CommandPaletteHost extensionHost, + string commandProviderId, + IServiceProvider serviceProvider) + : base(new TopLevelCommandWrapper( + commandItem.Unsafe?.Command ?? new NoOpCommand(), + extensionHost)) + { + _serviceProvider = serviceProvider; + _topLevelCommand = (TopLevelCommandWrapper)this.Command!; + _commandProviderId = commandProviderId; + + IsFallback = isFallback; + + // TODO: In reality, we should do an async fetch when we're created + // from an extension object. Probably have an + // `static async Task FromExtension(ExtensionObject)` + // or a + // `async Task PromoteStub(ExtensionObject)` + Model = commandItem; + try + { + var model = Model.Unsafe; + if (model == null) + { + return; + } + + _topLevelCommand.UnsafeInitializeProperties(); + + _idFromModel = _topLevelCommand.Id; + + Title = model.Title; + Subtitle = model.Subtitle; + Icon = model.Icon; + MoreCommands = model.MoreCommands; + + model.PropChanged += Model_PropChanged; + _topLevelCommand.PropChanged += Model_PropChanged; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + + GenerateId(); + + UpdateAlias(); + UpdateHotkey(); + UpdateTags(); + } + + private void GenerateId() + { + // Use WyHash64 to generate stable ID hashes. + // manually seeding with 0, so that the hash is stable across launches + var result = WyHash64.ComputeHash64(_commandProviderId + Title + Subtitle, seed: 0); + _generatedId = $"{_commandProviderId}{result}"; + } + + public void UpdateAlias(CommandAlias? newAlias) + { + _serviceProvider.GetService()!.UpdateAlias(Id, newAlias); + UpdateAlias(); + UpdateTags(); + } + + private void UpdateAlias() + { + // Add tags for the alias, if we have one. + var aliases = _serviceProvider.GetService(); + if (aliases != null) + { + Alias = aliases.AliasFromId(Id); + } + } + + private void UpdateHotkey() + { + var settings = _serviceProvider.GetService()!; + var hotkey = settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault(); + if (hotkey != null) + { + _hotkey = hotkey.Hotkey; + } + } + + private void UpdateTags() + { + var tags = new List(); + + if (Hotkey != null) + { + tags.Add(new Tag() { Text = Hotkey.ToString() }); + } + + if (Alias != null) + { + tags.Add(new Tag() { Text = Alias.SearchPrefix }); + } + + this.Tags = tags.ToArray(); + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + var propertyName = args.PropertyName; + var model = Model.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(_topLevelCommand.Name): + case nameof(Title): + this.Title = model.Title; + break; + case nameof(Subtitle): + this.Subtitle = model.Subtitle; + break; + case nameof(Icon): + var listIcon = model.Icon; + Icon = model.Icon; + break; + case nameof(MoreCommands): + this.MoreCommands = model.MoreCommands; + break; + case nameof(Command): + this.Command = model.Command; + break; + } + } + catch + { + } + } + + public void TryUpdateFallbackText(string newQuery) + { + if (!IsFallback) + { + return; + } + + _ = Task.Run(() => + { + try + { + var model = Model.Unsafe; + if (model is IFallbackCommandItem fallback) + { + var wasEmpty = string.IsNullOrEmpty(Title); + fallback.FallbackHandler.UpdateQuery(newQuery); + var isEmpty = string.IsNullOrEmpty(Title); + if (wasEmpty != isEmpty) + { + WeakReferenceMessenger.Default.Send(); + } + } + } + catch (Exception) + { + } + }); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs new file mode 100644 index 0000000000..6854b16144 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class TopLevelCommandManager : ObservableObject, + IRecipient +{ + private readonly IServiceProvider _serviceProvider; + private readonly TaskScheduler _taskScheduler; + + private readonly List _builtInCommands = []; + private readonly List _extensionCommandProviders = []; + + public TopLevelCommandManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _taskScheduler = _serviceProvider.GetService()!; + WeakReferenceMessenger.Default.Register(this); + } + + public ObservableCollection TopLevelCommands { get; set; } = []; + + [ObservableProperty] + public partial bool IsLoading { get; private set; } = true; + + public IEnumerable CommandProviders => _builtInCommands.Concat(_extensionCommandProviders); + + public async Task LoadBuiltinsAsync() + { + _builtInCommands.Clear(); + + // Load built-In commands first. These are all in-proc, and + // owned by our ServiceProvider. + var builtInCommands = _serviceProvider.GetServices(); + foreach (var provider in builtInCommands) + { + CommandProviderWrapper wrapper = new(provider, _taskScheduler); + _builtInCommands.Add(wrapper); + await LoadTopLevelCommandsFromProvider(wrapper); + } + + return true; + } + + // May be called from a background thread + private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider) + { + await commandProvider.LoadTopLevelCommands(); + + var makeAndAdd = (ICommandItem? i, bool fallback) => + { + TopLevelCommandItemWrapper wrapper = new( + new(i), fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, _serviceProvider); + lock (TopLevelCommands) + { + TopLevelCommands.Add(wrapper); + } + }; + + await Task.Factory.StartNew( + () => + { + foreach (var i in commandProvider.TopLevelItems) + { + makeAndAdd(i, false); + } + + foreach (var i in commandProvider.FallbackItems) + { + makeAndAdd(i, true); + } + }, + CancellationToken.None, + TaskCreationOptions.None, + _taskScheduler); + + commandProvider.CommandsChanged += CommandProvider_CommandsChanged; + } + + // By all accounts, we're already on a background thread (the COM call + // to handle the event shouldn't be on the main thread.). But just to + // be sure we don't block the caller, hop off this thread + private void CommandProvider_CommandsChanged(CommandProviderWrapper sender, IItemsChangedEventArgs args) => + _ = Task.Run(async () => await UpdateCommandsForProvider(sender, args)); + + /// + /// Called when a command provider raises its ItemsChanged event. We'll + /// remove the old commands from the top-level list and try to put the new + /// ones in the same place in the list. + /// + /// The provider who's commands changed + /// the ItemsChangedEvent the provider raised + /// an awaitable task + private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args) + { + // Work on a clone of the list, so that we can just do one atomic + // update to the actual observable list at the end + List clone = [.. TopLevelCommands]; + List newItems = []; + var startIndex = -1; + var firstCommand = sender.TopLevelItems[0]; + var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length; + + // Tricky: all Commands from a single provider get added to the + // top-level list all together, in a row. So if we find just the first + // one, we can slice it out and insert the new ones there. + for (var i = 0; i < clone.Count; i++) + { + var wrapper = clone[i]; + try + { + var thisCommand = wrapper.Model.Unsafe; + if (thisCommand != null) + { + var isTheSame = thisCommand == firstCommand; + if (isTheSame) + { + startIndex = i; + break; + } + } + } + catch + { + } + } + + // Fetch the new items + await sender.LoadTopLevelCommands(); + foreach (var i in sender.TopLevelItems) + { + newItems.Add(new(new(i), false, sender.ExtensionHost, sender.ProviderId, _serviceProvider)); + } + + foreach (var i in sender.FallbackItems) + { + newItems.Add(new(new(i), true, sender.ExtensionHost, sender.ProviderId, _serviceProvider)); + } + + // Slice out the old commands + if (startIndex != -1) + { + clone.RemoveRange(startIndex, commandsToRemove); + } + else + { + // ... or, just stick them at the end (this is unexpected) + startIndex = clone.Count; + } + + // add the new commands into the list at the place we found the old ones + clone.InsertRange(startIndex, newItems); + + // now update the actual observable list with the new contents + ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); + } + + public async Task ReloadAllCommandsAsync() + { + IsLoading = true; + var extensionService = _serviceProvider.GetService()!; + await extensionService.SignalStopExtensionsAsync(); + lock (TopLevelCommands) + { + TopLevelCommands.Clear(); + } + + await LoadBuiltinsAsync(); + _ = Task.Run(LoadExtensionsAsync); + } + + // Load commands from our extensions. Called on a background thread. + // Currently, this + // * queries the package catalog, + // * starts all the extensions, + // * then fetches the top-level commands from them. + // TODO In the future, we'll probably abstract some of this away, to have + // separate extension tracking vs stub loading. + [RelayCommand] + public async Task LoadExtensionsAsync() + { + var extensionService = _serviceProvider.GetService()!; + + extensionService.OnExtensionAdded -= ExtensionService_OnExtensionAdded; + extensionService.OnExtensionRemoved -= ExtensionService_OnExtensionRemoved; + + var extensions = await extensionService.GetInstalledExtensionsAsync(); + _extensionCommandProviders.Clear(); + if (extensions != null) + { + await StartExtensionsAndGetCommands(extensions); + } + + extensionService.OnExtensionAdded += ExtensionService_OnExtensionAdded; + extensionService.OnExtensionRemoved += ExtensionService_OnExtensionRemoved; + + IsLoading = false; + + return true; + } + + private void ExtensionService_OnExtensionAdded(IExtensionService sender, IEnumerable extensions) + { + // When we get an extension install event, hop off to a BG thread + _ = Task.Run(async () => + { + // for each newly installed extension, start it and get commands + // from it. One single package might have more than one + // IExtensionWrapper in it. + await StartExtensionsAndGetCommands(extensions); + }); + } + + private async Task StartExtensionsAndGetCommands(IEnumerable extensions) + { + // TODO This most definitely needs a lock + foreach (var extension in extensions) + { + try + { + // start it ... + await extension.StartExtensionAsync(); + + // ... and fetch the command provider from it. + CommandProviderWrapper wrapper = new(extension, _taskScheduler); + _extensionCommandProviders.Add(wrapper); + await LoadTopLevelCommandsFromProvider(wrapper); + } + catch (Exception ex) + { + Logger.LogError(ex.ToString()); + } + } + } + + private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable extensions) + { + // When we get an extension uninstall event, hop off to a BG thread + _ = Task.Run( + async () => + { + // Then find all the top-level commands that belonged to that extension + List commandsToRemove = []; + lock (TopLevelCommands) + { + foreach (var extension in extensions) + { + foreach (var command in TopLevelCommands) + { + var host = command.ExtensionHost; + if (host?.Extension == extension) + { + commandsToRemove.Add(command); + } + } + } + } + + // Then back on the UI thread (remember, TopLevelCommands is + // Observable, so you can't touch it on the BG thread)... + await Task.Factory.StartNew( + () => + { + // ... remove all the deleted commands. + lock (TopLevelCommands) + { + if (commandsToRemove.Count != 0) + { + foreach (var deleted in commandsToRemove) + { + TopLevelCommands.Remove(deleted); + } + } + } + }, + CancellationToken.None, + TaskCreationOptions.None, + _taskScheduler); + }); + } + + public TopLevelCommandItemWrapper? LookupCommand(string id) + { + lock (TopLevelCommands) + { + foreach (var command in TopLevelCommands) + { + if (command.Id == id) + { + return command; + } + } + } + + return null; + } + + public void Receive(ReloadCommandsMessage message) => + ReloadAllCommandsAsync().ConfigureAwait(false); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs new file mode 100644 index 0000000000..b8981d612c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandWrapper.cs @@ -0,0 +1,74 @@ +// 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.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class TopLevelCommandWrapper : ICommand +{ + private readonly ExtensionObject _command; + + public event TypedEventHandler? PropChanged; + + public string Name { get; private set; } = string.Empty; + + public string Id { get; private set; } = string.Empty; + + public IIconInfo Icon { get; private set; } = new IconInfo(null); + + public ICommand Command => _command.Unsafe!; + + public CommandPaletteHost ExtensionHost { get; } + + public TopLevelCommandWrapper(ICommand command, CommandPaletteHost extensionHost) + { + _command = new(command); + ExtensionHost = extensionHost; + } + + public void UnsafeInitializeProperties() + { + var model = _command.Unsafe!; + + Name = model.Name; + Id = model.Id; + Icon = model.Icon; + + model.PropChanged += Model_PropChanged; + model.PropChanged += this.PropChanged; + } + + private void Model_PropChanged(object sender, IPropChangedEventArgs args) + { + try + { + var propertyName = args.PropertyName; + var model = _command.Unsafe; + if (model == null) + { + return; // throw? + } + + switch (propertyName) + { + case nameof(Name): + this.Name = model.Name; + break; + case nameof(Icon): + var listIcon = model.Icon; + Icon = model.Icon; + break; + } + + PropChanged?.Invoke(this, args); + } + catch + { + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelHotkey.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelHotkey.cs new file mode 100644 index 0000000000..64ffbdb461 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelHotkey.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using Microsoft.CmdPal.UI.ViewModels.Settings; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class TopLevelHotkey(HotkeySettings? hotkey, string commandId) +{ + public string CommandId { get; set; } = commandId; + + public HotkeySettings? Hotkey { get; set; } = hotkey; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs new file mode 100644 index 0000000000..7d7564de14 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -0,0 +1,96 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public sealed partial class TopLevelViewModel : ObservableObject +{ + private readonly SettingsModel _settings; + private readonly IServiceProvider _serviceProvider; + + // TopLevelCommandItemWrapper is a ListItem, but it's in-memory for the app already. + // We construct it either from data that we pulled from the cache, or from the + // extension, but the data in it is all in our process now. + private readonly TopLevelCommandItemWrapper _item; + + public IconInfoViewModel Icon { get; private set; } + + public string Title => _item.Title; + + public string Subtitle => _item.Subtitle; + + public HotkeySettings? Hotkey + { + get => _item.Hotkey; + set + { + _serviceProvider.GetService()!.UpdateHotkey(_item.Id, value); + _item.Hotkey = value; + Save(); + } + } + + private string _aliasText; + + public string AliasText + { + get => _aliasText; + set + { + if (SetProperty(ref _aliasText, value)) + { + UpdateAlias(); + } + } + } + + private bool _isDirectAlias; + + public bool IsDirectAlias + { + get => _isDirectAlias; + set + { + if (SetProperty(ref _isDirectAlias, value)) + { + UpdateAlias(); + } + } + } + + public TopLevelViewModel(TopLevelCommandItemWrapper item, SettingsModel settings, IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _settings = settings; + + _item = item; + Icon = new(item.Icon ?? item.Command?.Icon); + Icon.InitializeProperties(); + + var aliases = _serviceProvider.GetService()!; + _isDirectAlias = _item.Alias?.IsDirect ?? false; + _aliasText = _item.Alias?.Alias ?? string.Empty; + } + + private void Save() => SettingsModel.SaveSettings(_settings); + + private void UpdateAlias() + { + if (string.IsNullOrWhiteSpace(_aliasText)) + { + _item.UpdateAlias(null); + } + else + { + var newAlias = new CommandAlias(_aliasText, _item.Id, _isDirectAlias); + _item.UpdateAlias(newAlias); + } + + Save(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml new file mode 100644 index 0000000000..ffcca6b3a8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + 240 + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs new file mode 100644 index 0000000000..530f3d8b0b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.Ext.Apps; +using Microsoft.CmdPal.Ext.Bookmarks; +using Microsoft.CmdPal.Ext.Calc; +using Microsoft.CmdPal.Ext.Indexer; +using Microsoft.CmdPal.Ext.Registry; +using Microsoft.CmdPal.Ext.Shell; +using Microsoft.CmdPal.Ext.System; +using Microsoft.CmdPal.Ext.TimeDate; +using Microsoft.CmdPal.Ext.WebSearch; +using Microsoft.CmdPal.Ext.WindowsServices; +using Microsoft.CmdPal.Ext.WindowsSettings; +using Microsoft.CmdPal.Ext.WindowsTerminal; +using Microsoft.CmdPal.Ext.WindowWalker; +using Microsoft.CmdPal.Ext.WinGet; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; +using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. +namespace Microsoft.CmdPal.UI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : Application +{ + /// + /// Gets the current instance in use. + /// + public static new App Current => (App)Application.Current; + + public Window? AppWindow { get; private set; } + + /// + /// Gets the instance to resolve application services. + /// + public IServiceProvider Services { get; } + + /// + /// Initializes a new instance of the class. + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + Services = ConfigureServices(); + + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + AppWindow = new MainWindow(); + AppWindow.Activate(); + } + + /// + /// Configures the services for the application + /// + private static ServiceProvider ConfigureServices() + { + // TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463 + ServiceCollection services = new(); + + // Root services + services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext()); + + // Built-in Commands. Order matters - this is the order they'll be presented by default. + var allApps = new AllAppsCommandProvider(); + var winget = new WinGetExtensionCommandsProvider(); + var callback = allApps.LookupApp; + winget.SetAllLookup(callback); + services.AddSingleton(allApps); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // TODO GH #527 re-enable the clipboard commands + // services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(winget); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Models + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + var sm = SettingsModel.LoadSettings(); + services.AddSingleton(sm); + var state = AppStateModel.LoadState(); + services.AddSingleton(state); + services.AddSingleton(); + + // ViewModels + services.AddSingleton(); + + return services.BuildServiceProvider(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-100.png new file mode 100644 index 0000000000..23fcc65126 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-125.png new file mode 100644 index 0000000000..1187f547bb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-150.png new file mode 100644 index 0000000000..faba6bb5e9 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-200.png new file mode 100644 index 0000000000..200beaecab Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-400.png new file mode 100644 index 0000000000..487c4f055d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/BadgeLogo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-100.png new file mode 100644 index 0000000000..ca91e23aef Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-125.png new file mode 100644 index 0000000000..545767666b Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-150.png new file mode 100644 index 0000000000..3a5ae8a764 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-200.png new file mode 100644 index 0000000000..de17c39145 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-400.png new file mode 100644 index 0000000000..e73d2f8353 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/LargeTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-100.png new file mode 100644 index 0000000000..3fe683ddfc Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-125.png new file mode 100644 index 0000000000..3910e1ed6c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-150.png new file mode 100644 index 0000000000..fd6e366f76 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-200.png new file mode 100644 index 0000000000..b51b5effe3 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-400.png new file mode 100644 index 0000000000..fd0e233062 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SmallTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-100.png new file mode 100644 index 0000000000..afe7356665 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-125.png new file mode 100644 index 0000000000..cd28a64965 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-150.png new file mode 100644 index 0000000000..0e09bf375b Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-200.png new file mode 100644 index 0000000000..eae4572a73 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-400.png new file mode 100644 index 0000000000..8cb6e5e5ac Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/SplashScreen.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..9d5e1b85f6 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..f95946d70e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000..d3ef23e950 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..6e7ed14900 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000..8220fab6ce Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square150x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000000..916ca9f61f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000000..4da8b1dedb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000000..bd1fe6c5b5 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000000..ce23c67b84 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000000..f54348ba40 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000000..916ca9f61f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 0000000000..4da8b1dedb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000000..bd1fe6c5b5 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000000..ce23c67b84 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000000..f54348ba40 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000..5ee3858daa Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000..0ac194c90f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000..afaeca426c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..348627a3cd Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000..11cead2a88 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000..0b22cd1235 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000..593b285d8c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000..8e1bae363d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000..86883ff097 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..c1bdac2e9c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Square44x44Logo.targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.png new file mode 100644 index 0000000000..906803152e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-100.png new file mode 100644 index 0000000000..906803152e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-125.png new file mode 100644 index 0000000000..e7d17e270d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-150.png new file mode 100644 index 0000000000..1955f2680d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-200.png new file mode 100644 index 0000000000..bac5a60a9f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-400.png new file mode 100644 index 0000000000..7507a594dc Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/StoreLogo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000000..8fb0c6ed1e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000000..652650a28b Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000000..9716412bee Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..afe7356665 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000000..eae4572a73 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/Wide310x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/icon.ico b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/icon.ico new file mode 100644 index 0000000000..2b7765117f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Dev/icon.ico differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-100.png new file mode 100644 index 0000000000..66d9712d19 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-125.png new file mode 100644 index 0000000000..4b888cbfe1 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-150.png new file mode 100644 index 0000000000..fa51fa861b Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-200.png new file mode 100644 index 0000000000..209ea5f63a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-400.png new file mode 100644 index 0000000000..2c2008979d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LargeTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LockScreenLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-100.png new file mode 100644 index 0000000000..5505cf67e9 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-125.png new file mode 100644 index 0000000000..1abda3dd69 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-150.png new file mode 100644 index 0000000000..fb8a8394bf Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-200.png new file mode 100644 index 0000000000..77716b370e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-400.png new file mode 100644 index 0000000000..325d4660cb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SmallTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-100.png new file mode 100644 index 0000000000..d717ecf80c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-125.png new file mode 100644 index 0000000000..76d8fd345f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-150.png new file mode 100644 index 0000000000..9b02d2f956 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-200.png new file mode 100644 index 0000000000..8df93bc56f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-400.png new file mode 100644 index 0000000000..5ac3f0ed66 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/SplashScreen.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..f45a4b5453 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..e4181fae93 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000..71d106afcb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..ba42b403ba Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000..b9b0b0481d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square150x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000000..effd1b294a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000000..5d19c1fd21 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000000..5ae6512bbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000000..d059c1b2fb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000000..9c47f2e758 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000000..effd1b294a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000000..5ae6512bbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000000..d059c1b2fb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000000..9c47f2e758 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000..1afd204d26 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000..367e0540d6 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000..a50b45df0d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..1b72c87526 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000..eb3e051dbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000..e70028d83f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000..96ae3ddb0c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..5d19c1fd21 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000..935a2b6dab Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000..9ae2093373 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..3c7ace9152 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Square44x44Logo.targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-100.png new file mode 100644 index 0000000000..5509cf4193 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-125.png new file mode 100644 index 0000000000..cc3613dc57 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-150.png new file mode 100644 index 0000000000..98e3c0f70c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-200.png new file mode 100644 index 0000000000..8e0a3c570e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-400.png new file mode 100644 index 0000000000..2baa82f575 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/StoreLogo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000000..cac24c0150 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000000..4bf18758c5 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000000..e1c27b7366 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..d717ecf80c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000000..8df93bc56f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/Wide310x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/icon.ico b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/icon.ico new file mode 100644 index 0000000000..d0fcb15642 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/Stable/icon.ico differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props new file mode 100644 index 0000000000..d99688c081 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props @@ -0,0 +1,48 @@ + + + + + Stable + Stable + Stable + Dev + + + + Assets\Stable\icon.ico + + + Assets\Stable\icon.ico + + + Assets\Stable\icon.ico + + + Assets\Dev\icon.ico + + + + + + + true + Assets\%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props new file mode 100644 index 0000000000..c732a29374 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props @@ -0,0 +1,13 @@ + + + + + + Release + + + + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/AdaptiveCardsConfig.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/AdaptiveCardsConfig.cs new file mode 100644 index 0000000000..05cc245fef --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/AdaptiveCardsConfig.cs @@ -0,0 +1,342 @@ +// 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 AdaptiveCards.Rendering.WinUI3; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed class AdaptiveCardsConfig +{ + public static AdaptiveHostConfig Light { get; } + + public static AdaptiveHostConfig Dark { get; } + + static AdaptiveCardsConfig() + { + Light = AdaptiveHostConfig.FromJsonString(LightHostConfigString).HostConfig; + Dark = AdaptiveHostConfig.FromJsonString(DarkHostConfigString).HostConfig; + } + + public static readonly string DarkHostConfigString = """ +{ + "spacing": { + "small": 4, + "default": 8, + "medium": 20, + "large": 30, + "extraLarge": 40, + "padding": 8 + }, + "separator": { + "lineThickness": 0, + "lineColor": "#C8FFFFFF" + }, + "supportsInteractivity": true, + "fontTypes": { + "default": { + "fontFamily": "'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + "fontSizes": { + "small": 12, + "default": 12, + "medium": 14, + "large": 20, + "extraLarge": 26 + }, + "fontWeights": { + "lighter": 200, + "default": 400, + "bolder": 600 + } + }, + "monospace": { + "fontFamily": "'Courier New', Courier, monospace", + "fontSizes": { + "small": 12, + "default": 12, + "medium": 14, + "large": 18, + "extraLarge": 26 + }, + "fontWeights": { + "lighter": 200, + "default": 400, + "bolder": 600 + } + } + }, + "containerStyles": { + "default": { + "backgroundColor": "#00000000", + "borderColor": "#00000000", + "foregroundColors": { + "default": { + "default": "#FFFFFF", + "subtle": "#C8FFFFFF" + }, + "accent": { + "default": "#0063B1", + "subtle": "#880063B1" + }, + "attention": { + "default": "#FF5555", + "subtle": "#DDFF5555" + }, + "good": { + "default": "#54a254", + "subtle": "#DD54a254" + }, + "warning": { + "default": "#c3ab23", + "subtle": "#DDc3ab23" + } + } + }, + "emphasis": { + "backgroundColor": "#09FFFFFF", + "borderColor": "#09FFFFFF", + "foregroundColors": { + "default": { + "default": "#FFFFFF", + "subtle": "#C8FFFFFF" + }, + "accent": { + "default": "#2E89FC", + "subtle": "#882E89FC" + }, + "attention": { + "default": "#FF5555", + "subtle": "#DDFF5555" + }, + "good": { + "default": "#54a254", + "subtle": "#DD54a254" + }, + "warning": { + "default": "#c3ab23", + "subtle": "#DDc3ab23" + } + } + } + }, + "imageSizes": { + "small": 16, + "medium": 24, + "large": 32 + }, + "actions": { + "maxActions": 5, + "spacing": "default", + "buttonSpacing": 8, + "showCard": { + "actionMode": "inline", + "inlineTopMargin": 8 + }, + "actionsOrientation": "horizontal", + "actionAlignment": "stretch" + }, + "adaptiveCard": { + "allowCustomStyle": false + }, + "imageSet": { + "imageSize": "medium", + "maxImageHeight": 100 + }, + "factSet": { + "title": { + "color": "default", + "size": "default", + "isSubtle": false, + "weight": "bolder", + "wrap": true, + "maxWidth": 150 + }, + "value": { + "color": "default", + "size": "default", + "isSubtle": false, + "weight": "default", + "wrap": true + }, + "spacing": 8 + }, + "textStyles": { + "heading": { + "size": "large", + "weight": "bolder", + "color": "default", + "isSubtle": false, + "fontType": "default" + }, + "columnHeader": { + "size": "medium", + "weight": "bolder", + "color": "default", + "isSubtle": false, + "fontType": "default" + } + } +} +"""; + + public static readonly string LightHostConfigString = """ +{ + "spacing": { + "small": 4, + "default": 8, + "medium": 20, + "large": 30, + "extraLarge": 40, + "padding": 8 + }, + "separator": { + "lineThickness": 0, + "lineColor": "#606060" + }, + "supportsInteractivity": true, + "fontTypes": { + "default": { + "fontFamily": "'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + "fontSizes": { + "small": 12, + "default": 12, + "medium": 14, + "large": 20, + "extraLarge": 26 + }, + "fontWeights": { + "lighter": 200, + "default": 400, + "bolder": 600 + } + }, + "monospace": { + "fontFamily": "'Courier New', Courier, monospace", + "fontSizes": { + "small": 12, + "default": 12, + "medium": 14, + "large": 18, + "extraLarge": 26 + }, + "fontWeights": { + "lighter": 200, + "default": 400, + "bolder": 600 + } + } + }, + "containerStyles": { + "default": { + "backgroundColor": "#00000000", + "borderColor": "#00000000", + "foregroundColors": { + "default": { + "default": "#E6000000", + "subtle": "#99000000" + }, + "accent": { + "default": "#0063B1", + "subtle": "#880063B1" + }, + "attention": { + "default": "#C00000", + "subtle": "#DDC00000" + }, + "good": { + "default": "#54a254", + "subtle": "#DD54a254" + }, + "warning": { + "default": "#c3ab23", + "subtle": "#DDc3ab23" + } + } + }, + "emphasis": { + "backgroundColor": "#80F6F6F6", + "borderColor": "#80F6F6F6", + "foregroundColors": { + "default": { + "default": "#E6000000", + "subtle": "#99000000" + }, + "accent": { + "default": "#2E89FC", + "subtle": "#882E89FC" + }, + "attention": { + "default": "#C00000", + "subtle": "#DDC00000" + }, + "good": { + "default": "#54a254", + "subtle": "#DD54a254" + }, + "warning": { + "default": "#c3ab23", + "subtle": "#DDc3ab23" + } + } + } + }, + "imageSizes": { + "small": 16, + "medium": 24, + "large": 32 + }, + "actions": { + "maxActions": 5, + "spacing": "default", + "buttonSpacing": 8, + "showCard": { + "actionMode": "inline", + "inlineTopMargin": 8 + }, + "actionsOrientation": "horizontal", + "actionAlignment": "stretch" + }, + "adaptiveCard": { + "allowCustomStyle": false + }, + "imageSet": { + "imageSize": "medium", + "maxImageHeight": 100 + }, + "factSet": { + "title": { + "color": "default", + "size": "default", + "isSubtle": false, + "weight": "bolder", + "wrap": true, + "maxWidth": 150 + }, + "value": { + "color": "default", + "size": "default", + "isSubtle": false, + "weight": "default", + "wrap": true + }, + "spacing": 8 + }, + "textStyles": { + "heading": { + "size": "large", + "weight": "bolder", + "color": "default", + "isSubtle": false, + "fontType": "default" + }, + "columnHeader": { + "size": "medium", + "weight": "bolder", + "color": "default", + "isSubtle": false, + "fontType": "default" + } + } +} +"""; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml new file mode 100644 index 0000000000..4bc9692b31 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs new file mode 100644 index 0000000000..382e7ba7ea --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs @@ -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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.Views; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Input; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class CommandBar : UserControl, + IRecipient, + ICurrentPageAware +{ + public CommandBarViewModel ViewModel { get; set; } = new(); + + public PageViewModel? CurrentPageViewModel + { + get => (PageViewModel?)GetValue(CurrentPageViewModelProperty); + set => SetValue(CurrentPageViewModelProperty, value); + } + + // Using a DependencyProperty as the backing store for CurrentPage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CurrentPageViewModelProperty = + DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(CommandBar), new PropertyMetadata(null)); + + public CommandBar() + { + this.InitializeComponent(); + + // RegisterAll isn't AOT compatible + WeakReferenceMessenger.Default.Register(this); + } + + public void Receive(OpenContextMenuMessage message) + { + if (!ViewModel.ShouldShowContextMenu) + { + return; + } + + var options = new FlyoutShowOptions + { + ShowMode = FlyoutShowMode.Standard, + }; + MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options); + CommandsDropdown.SelectedIndex = 0; + CommandsDropdown.Focus(FocusState.Programmatic); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] + private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e) + { + ViewModel.InvokePrimaryCommand(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] + private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) + { + ViewModel.InvokeSecondaryCommand(); + } + + private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e) + { + if (CurrentPageViewModel?.StatusMessages.Count > 0) + { + StatusMessagesFlyout.ShowAt( + placementTarget: IconRoot, + showOptions: new FlyoutShowOptions() { ShowMode = FlyoutShowMode.Standard }); + } + } + + private void SettingsIcon_Tapped(object sender, TappedRoutedEventArgs e) + { + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + + private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e) + { + if (e.ClickedItem is CommandContextItemViewModel item) + { + ViewModel?.InvokeItemCommand.Execute(item); + MoreCommandsButton.Flyout.Hide(); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml new file mode 100644 index 0000000000..48784a997f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs new file mode 100644 index 0000000000..68209d750a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs @@ -0,0 +1,106 @@ +// 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 AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ContentFormControl : UserControl +{ + private static readonly AdaptiveCardRenderer _renderer; + private ContentFormViewModel? _viewModel; + + // LOAD-BEARING: if you don't hang onto a reference to the RenderedAdaptiveCard + // then the GC might clean it up sometime, even while the card is in the UI + // tree. If this gets GC'd, then it'll revoke our Action handler, and the + // form will do seemingly nothing. + private RenderedAdaptiveCard? _renderedCard; + + public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); } + + static ContentFormControl() + { + // We can't use `CardOverrideStyles` here yet, because we haven't called InitializeComponent once. + // But also, the default value isn't `null` here. It's... some other default empty value. + // So clear it out so that we know when the first time we get created is + _renderer = new AdaptiveCardRenderer() + { + OverrideStyles = null, + }; + } + + public ContentFormControl() + { + this.InitializeComponent(); + var lightTheme = ActualTheme == Microsoft.UI.Xaml.ElementTheme.Light; + _renderer.HostConfig = lightTheme ? AdaptiveCardsConfig.Light : AdaptiveCardsConfig.Dark; + + // 5% BODGY: if we set this multiple times over the lifetime of the app, + // then the second call will explode, because "CardOverrideStyles is already the child of another element". + // SO only set this once. + if (_renderer.OverrideStyles == null) + { + _renderer.OverrideStyles = CardOverrideStyles; + } + + // TODO in the future, we should handle ActualThemeChanged and replace + // our rendered card with one for that theme. But today is not that day + } + + private void AttachViewModel(ContentFormViewModel? vm) + { + if (_viewModel != null) + { + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + } + + _viewModel = vm; + + if (_viewModel != null) + { + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + + var c = _viewModel.Card; + if (c != null) + { + DisplayCard(c); + } + } + } + + private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (ViewModel == null) + { + return; + } + + if (e.PropertyName == nameof(ViewModel.Card)) + { + var c = ViewModel.Card; + if (c != null) + { + DisplayCard(c); + } + } + } + + private void DisplayCard(AdaptiveCardParseResult result) + { + _renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard); + ContentGrid.Children.Clear(); + if (_renderedCard.FrameworkElement != null) + { + ContentGrid.Children.Add(_renderedCard.FrameworkElement); + } + + _renderedCard.Action += Rendered_Action; + } + + private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) => + ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson()); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs new file mode 100644 index 0000000000..286548f96a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI; +using Microsoft.CmdPal.UI.Deferred; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.Controls; + +/// +/// A helper control which takes an and creates the corresponding . +/// +public partial class ContentIcon : FontIcon +{ + public UIElement Content + { + get => (UIElement)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public static readonly DependencyProperty ContentProperty = + DependencyProperty.Register( + nameof(Content), + typeof(UIElement), + typeof(ContentIcon), + new PropertyMetadata(null)); + + public ContentIcon() + { + Loaded += IconBoxElement_Loaded; + } + + private void IconBoxElement_Loaded(object sender, RoutedEventArgs e) + { + if (this.FindDescendants().OfType().FirstOrDefault() is Grid grid && Content is not null) + { + grid.Children.Add(Content); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconBox.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconBox.cs new file mode 100644 index 0000000000..34f0683440 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconBox.cs @@ -0,0 +1,159 @@ +// 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.UI.Deferred; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.Controls; + +/// +/// A helper control which takes an and creates the corresponding . +/// +public partial class IconBox : ContentControl +{ + private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); + + /// + /// Gets or sets the to display within the . Overwritten, if is used instead. + /// + public IconSource? Source + { + get => (IconSource?)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + // Using a DependencyProperty as the backing store for Source. This enables animation, styling, binding, etc... + public static readonly DependencyProperty SourceProperty = + DependencyProperty.Register(nameof(Source), typeof(IconSource), typeof(IconBox), new PropertyMetadata(null, OnSourcePropertyChanged)); + + /// + /// Gets or sets a value to use as the to retrieve an to set as the . + /// + public object? SourceKey + { + get => (object?)GetValue(SourceKeyProperty); + set => SetValue(SourceKeyProperty, value); + } + + // Using a DependencyProperty as the backing store for SourceKey. This enables animation, styling, binding, etc... + public static readonly DependencyProperty SourceKeyProperty = + DependencyProperty.Register(nameof(SourceKey), typeof(object), typeof(IconBox), new PropertyMetadata(null, OnSourceKeyPropertyChanged)); + + /// + /// Gets or sets the event handler to provide the value of the for the property from the provided . + /// + public event TypedEventHandler? SourceRequested; + + private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is IconBox @this) + { + switch (e.NewValue) + { + case null: + @this.Content = null; + break; + case FontIconSource fontIco: + fontIco.FontSize = double.IsNaN(@this.Width) ? @this.Height : @this.Width; + + // For inexplicable reasons, FontIconSource.CreateIconElement + // doesn't work, so do it ourselves + // TODO: File platform bug? + IconSourceElement elem = new() + { + IconSource = fontIco, + }; + @this.Content = elem; + break; + case IconSource source: + @this.Content = source.CreateIconElement(); + break; + default: + throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource."); + } + } + } + + private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is IconBox @this) + { + if (e.NewValue == null) + { + @this.Source = null; + } + else + { + // TODO GH #239 switch back when using the new MD text block + // _ = @this._queue.EnqueueAsync(() => + @this._queue.TryEnqueue(new(async () => + { + var requestedTheme = @this.ActualTheme; + var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme); + + if (@this.SourceRequested != null) + { + await @this.SourceRequested.InvokeAsync(@this, eventArgs); + + // After the await: + // Is the icon we're looking up now, the one we still + // want to find? Since this IconBox might be used in a + // list virtualization situation, it's very possible we + // may have already been set to a new icon before we + // even got back from the await. + if (eventArgs.Key != @this.SourceKey) + { + // If the requested icon has changed, then just bail + return; + } + + @this.Source = eventArgs.Value; + + // Here's a little lesson in trickery: + // Emoji are rendered just a bit bigger than Segoe Icons. + // Just enough bigger that they get clipped if you put + // them in a box at the same size. + // + // So, if the icon we get back was a font icon, + // and the glyph for that icon is NOT in the range of + // Segoe icons, then let's give the icon some extra space + @this.Padding = new Thickness(0); + + IconDataViewModel? iconData = null; + if (eventArgs.Key is IconDataViewModel) + { + iconData = eventArgs.Key as IconDataViewModel; + } + else if (eventArgs.Key is IconInfoViewModel info) + { + iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark; + } + + if (iconData != null && + @this.Source is FontIconSource) + { + if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2) + { + var ch = iconData.Icon[0]; + + // The range of MDL2 Icons isn't explicitly defined, but + // we're using this based off the table on: + // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font + var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF'; + if (!isMDL2Icon) + { + @this.Padding = new Thickness(-4); + } + } + } + } + })); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.cs new file mode 100644 index 0000000000..609bcec62e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.cs @@ -0,0 +1,179 @@ +// 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.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; +using Windows.System; + +namespace Microsoft.CmdPal.UI.Controls; + +[TemplatePart(Name = KeyPresenter, Type = typeof(ContentPresenter))] +[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")] +[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")] +[TemplateVisualState(Name = "Default", GroupName = "StateStates")] +[TemplateVisualState(Name = "Error", GroupName = "StateStates")] +public sealed partial class KeyVisual : Control +{ + private const string KeyPresenter = "KeyPresenter"; + private KeyVisual? _keyVisual; + private ContentPresenter _keyPresenter = new(); + + public object Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged)); + + public VisualType VisualType + { + get => (VisualType)GetValue(VisualTypeProperty); + set => SetValue(VisualTypeProperty, value); + } + + public static readonly DependencyProperty VisualTypeProperty = DependencyProperty.Register("VisualType", typeof(VisualType), typeof(KeyVisual), new PropertyMetadata(default(VisualType), OnSizeChanged)); + + public bool IsError + { + get => (bool)GetValue(IsErrorProperty); + set => SetValue(IsErrorProperty, value); + } + + public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsErrorChanged)); + + public KeyVisual() + { + this.DefaultStyleKey = typeof(KeyVisual); + this.Style = GetStyleSize("TextKeyVisualStyle"); + } + + protected override void OnApplyTemplate() + { + IsEnabledChanged -= KeyVisual_IsEnabledChanged; + _keyVisual = this; + _keyPresenter = (ContentPresenter)_keyVisual.GetTemplateChild(KeyPresenter); + Update(); + SetEnabledState(); + SetErrorState(); + IsEnabledChanged += KeyVisual_IsEnabledChanged; + base.OnApplyTemplate(); + } + + private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((KeyVisual)d).Update(); + } + + private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((KeyVisual)d).Update(); + } + + private static void OnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((KeyVisual)d).SetErrorState(); + } + + private void Update() + { + if (_keyVisual == null) + { + return; + } + + if (_keyVisual.Content != null) + { + if (_keyVisual.Content.GetType() == typeof(string)) + { + _keyVisual.Style = GetStyleSize("TextKeyVisualStyle"); + _keyVisual._keyPresenter.Content = _keyVisual.Content; + } + else + { + _keyVisual.Style = GetStyleSize("IconKeyVisualStyle"); + + switch ((int)_keyVisual.Content) + { + /* We can enable other glyphs in the future + case 13: // The Enter key or button. + _keyVisual._keyPresenter.Content = "\uE751"; break; + + case 8: // The Back key or button. + _keyVisual._keyPresenter.Content = "\uE750"; break; + + case 16: // The right Shift key or button. + case 160: // The left Shift key or button. + case 161: // The Shift key or button. + _keyVisual._keyPresenter.Content = "\uE752"; break; */ + + case 38: _keyVisual._keyPresenter.Content = "\uE0E4"; break; // The Up Arrow key or button. + case 40: _keyVisual._keyPresenter.Content = "\uE0E5"; break; // The Down Arrow key or button. + case 37: _keyVisual._keyPresenter.Content = "\uE0E2"; break; // The Left Arrow key or button. + case 39: _keyVisual._keyPresenter.Content = "\uE0E3"; break; // The Right Arrow key or button. + + case 91: // The left Windows key + case 92: // The right Windows key + var winIcon = XamlReader.Load(@"") as PathIcon; + var winIconContainer = new Viewbox + { + Child = winIcon, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + + var iconDimensions = GetIconSize(); + winIconContainer.Height = iconDimensions; + winIconContainer.Width = iconDimensions; + _keyVisual._keyPresenter.Content = winIconContainer; + break; + default: _keyVisual._keyPresenter.Content = ((VirtualKey)_keyVisual.Content).ToString(); break; + } + } + } + } + + public Style GetStyleSize(string styleName) + { + return VisualType == VisualType.Small + ? (Style)App.Current.Resources["Small" + styleName] + : VisualType == VisualType.SmallOutline + ? (Style)App.Current.Resources["SmallOutline" + styleName] + : VisualType == VisualType.TextOnly + ? (Style)App.Current.Resources["Only" + styleName] + : (Style)App.Current.Resources["Default" + styleName]; + } + + public double GetIconSize() + { + return VisualType == VisualType.Small || VisualType == VisualType.SmallOutline + ? (double)App.Current.Resources["SmallIconSize"] + : (double)App.Current.Resources["DefaultIconSize"]; + } + + private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + SetEnabledState(); + } + + private void SetErrorState() + { + VisualStateManager.GoToState(this, IsError ? "Error" : "Default", true); + } + + private void SetEnabledState() + { + VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true); + } +} + +public enum VisualType +{ + Small, + SmallOutline, + TextOnly, + Large, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.xaml new file mode 100644 index 0000000000..7364ea8eec --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/KeyVisual/KeyVisual.xaml @@ -0,0 +1,174 @@ + + + 16 + 12 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml new file mode 100644 index 0000000000..379ea6b03d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs new file mode 100644 index 0000000000..c1c679fc73 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -0,0 +1,271 @@ +// 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; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.Views; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using CoreVirtualKeyStates = Windows.UI.Core.CoreVirtualKeyStates; +using VirtualKey = Windows.System.VirtualKey; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class SearchBar : UserControl, + IRecipient, + IRecipient, + ICurrentPageAware +{ + private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); + + /// + /// Gets the that we create to track keyboard input and throttle/debounce before we make queries. + /// + private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + private bool _isBackspaceHeld; + + public PageViewModel? CurrentPageViewModel + { + get => (PageViewModel?)GetValue(CurrentPageViewModelProperty); + set => SetValue(CurrentPageViewModelProperty, value); + } + + // Using a DependencyProperty as the backing store for CurrentPageViewModel. This enables animation, styling, binding, etc... + public static readonly DependencyProperty CurrentPageViewModelProperty = + DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(SearchBar), new PropertyMetadata(null, OnCurrentPageViewModelChanged)); + + private static void OnCurrentPageViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + //// TODO: If the Debounce timer hasn't fired, we may want to store the current Filter in the OldValue/prior VM, but we don't want that to go actually do work... + var @this = (SearchBar)d; + + if (@this != null + && e.OldValue is PageViewModel old) + { + old.PropertyChanged -= @this.Page_PropertyChanged; + } + + if (@this != null + && e.NewValue is PageViewModel page) + { + // TODO: In some cases we probably want commands to clear a filter + // somewhere in the process, so we need to figure out when that is. + @this.FilterBox.Text = page.Filter; + @this.FilterBox.Select(@this.FilterBox.Text.Length, 0); + + page.PropertyChanged += @this.Page_PropertyChanged; + } + } + + public SearchBar() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + } + + public void ClearSearch() + { + // TODO GH #239 switch back when using the new MD text block + // _ = _queue.EnqueueAsync(() => + _queue.TryEnqueue(new(() => + { + this.FilterBox.Text = string.Empty; + + if (CurrentPageViewModel != null) + { + CurrentPageViewModel.Filter = string.Empty; + } + })); + } + + public void SelectSearch() + { + // TODO GH #239 switch back when using the new MD text block + // _ = _queue.EnqueueAsync(() => + _queue.TryEnqueue(new(() => + { + this.FilterBox.SelectAll(); + })); + } + + private void FilterBox_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Handled) + { + return; + } + + var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + if (e.Key == VirtualKey.Down) + { + WeakReferenceMessenger.Default.Send(); + + e.Handled = true; + } + else if (e.Key == VirtualKey.Up) + { + WeakReferenceMessenger.Default.Send(); + + e.Handled = true; + } + else if (ctrlPressed && e.Key == VirtualKey.Enter) + { + // ctrl+enter + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + else if (e.Key == VirtualKey.Enter) + { + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + else if (ctrlPressed && e.Key == VirtualKey.K) + { + // ctrl+k + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + else if (e.Key == VirtualKey.Right) + { + if (CurrentPageViewModel != null && !string.IsNullOrEmpty(CurrentPageViewModel.TextToSuggest)) + { + FilterBox.Text = CurrentPageViewModel.TextToSuggest; + FilterBox.Select(FilterBox.Text.Length, 0); + e.Handled = true; + } + } + else if (e.Key == VirtualKey.Escape) + { + if (string.IsNullOrEmpty(FilterBox.Text)) + { + WeakReferenceMessenger.Default.Send(new()); + } + else + { + // Clear the search box + FilterBox.Text = string.Empty; + + // hack TODO GH #245 + if (CurrentPageViewModel != null) + { + CurrentPageViewModel.Filter = FilterBox.Text; + } + } + + e.Handled = true; + } + else if (e.Key == VirtualKey.Back) + { + // hack TODO GH #245 + if (CurrentPageViewModel != null) + { + CurrentPageViewModel.Filter = FilterBox.Text; + } + } + else if (e.Key == VirtualKey.Left && altPressed) + { + WeakReferenceMessenger.Default.Send(new()); + } + } + + private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Back) + { + if (string.IsNullOrEmpty(FilterBox.Text)) + { + if (!_isBackspaceHeld) + { + // Navigate back on single backspace when empty + WeakReferenceMessenger.Default.Send(new(true)); + } + + e.Handled = true; + } + else + { + // Mark backspace as held to handle continuous deletion + _isBackspaceHeld = true; + } + } + } + + private void FilterBox_PreviewKeyUp(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Back) + { + // Reset the backspace state on key release + _isBackspaceHeld = false; + } + } + + private void FilterBox_TextChanged(object sender, TextChangedEventArgs e) + { + Debug.WriteLine($"FilterBox_TextChanged: {FilterBox.Text}"); + + // TERRIBLE HACK TODO GH #245 + // There's weird wacky bugs with debounce currently. We're trying + // to get them ingested, but while we wait for the toolkit feeds to + // bubble, just manually send the first character, always + // (otherwise aliases just stop working) + if (FilterBox.Text.Length == 1) + { + if (CurrentPageViewModel != null) + { + CurrentPageViewModel.Filter = FilterBox.Text; + } + + return; + } + + // TODO: We could encapsulate this in a Behavior if we wanted to bind to the Filter property. + _debounceTimer.Debounce( + () => + { + // Actually plumb Filtering to the viewmodel + if (CurrentPageViewModel != null) + { + CurrentPageViewModel.Filter = FilterBox.Text; + } + }, + //// Couldn't find a good recommendation/resource for value here. PT uses 50ms as default, so that is a reasonable default + //// This seems like a useful testing site for typing times: https://keyboardtester.info/keyboard-latency-test/ + //// i.e. if another keyboard press comes in within 50ms of the last, we'll wait before we fire off the request + interval: TimeSpan.FromMilliseconds(50), + //// If we're not already waiting, and this is blanking out or the first character type, we'll start filtering immediately instead to appear more responsive and either clear the filter to get back home faster or at least chop to the first starting letter. + immediate: FilterBox.Text.Length <= 1); + } + + // Used to handle the case when a ListPage's `SearchText` may have changed + private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var property = e.PropertyName; + if (CurrentPageViewModel is ListViewModel list && + property == nameof(ListViewModel.SearchText)) + { + // Only if the text actually changed... + // (sometimes this triggers on a round-trip of the SearchText) + if (FilterBox.Text != list.SearchText) + { + // ... Update our displayed text, and... + FilterBox.Text = list.SearchText; + + // ... Move the cursor to the end of the input + FilterBox.Select(FilterBox.Text.Length, 0); + } + } + } + + public void Receive(GoHomeMessage message) => ClearSearch(); + + public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/HotkeySettingsControlHook.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/HotkeySettingsControlHook.cs new file mode 100644 index 0000000000..66f482f502 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/HotkeySettingsControlHook.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using PowerToys.Interop; + +namespace Microsoft.CmdPal.UI.Library; + +public delegate void KeyEvent(int key); + +public delegate bool IsActive(); + +public delegate bool FilterAccessibleKeyboardEvents(int key, UIntPtr extraInfo); + +public class HotkeySettingsControlHook : IDisposable +{ + private const int WmKeyDown = 0x100; + private const int WmKeyUp = 0x101; + private const int WmSysKeyDown = 0x0104; + private const int WmSysKeyUp = 0x0105; + + private readonly KeyboardHook _hook; + private readonly KeyEvent _keyDown; + private readonly KeyEvent _keyUp; + private readonly IsActive _isActive; + + private readonly FilterAccessibleKeyboardEvents _filterKeyboardEvent; + + private bool disposedValue; + + public HotkeySettingsControlHook(KeyEvent keyDown, KeyEvent keyUp, IsActive isActive, FilterAccessibleKeyboardEvents filterAccessibleKeyboardEvents) + { + _keyDown = keyDown; + _keyUp = keyUp; + _isActive = isActive; + _filterKeyboardEvent = filterAccessibleKeyboardEvents; + _hook = new KeyboardHook(HotkeySettingsHookCallback, IsActive, FilterKeyboardEvents); + _hook.Start(); + } + + private bool IsActive() + { + return _isActive(); + } + + private void HotkeySettingsHookCallback(KeyboardEvent ev) + { + switch (ev.message) + { + case WmKeyDown: + case WmSysKeyDown: + _keyDown(ev.key); + break; + case WmKeyUp: + case WmSysKeyUp: + _keyUp(ev.key); + break; + } + } + + private bool FilterKeyboardEvents(KeyboardEvent ev) + { +#pragma warning disable CA2020 // Prevent from behavioral change + return _filterKeyboardEvent(ev.key, (UIntPtr)ev.dwExtraInfo); +#pragma warning restore CA2020 // Prevent from behavioral change + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // Dispose the KeyboardHook object to terminate the hook threads + _hook.Dispose(); + } + + disposedValue = true; + } + } + + public bool GetDisposedState() => disposedValue; + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeKeyboardHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeKeyboardHelper.cs new file mode 100644 index 0000000000..9d3961f907 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeKeyboardHelper.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + internal static class NativeKeyboardHelper + { + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct INPUT + { + internal INPUTTYPE type; + internal InputUnion data; + + internal static int Size + { + get { return Marshal.SizeOf(typeof(INPUT)); } + } + } + + [StructLayout(LayoutKind.Explicit)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct InputUnion + { + [FieldOffset(0)] + internal MOUSEINPUT mi; + [FieldOffset(0)] + internal KEYBDINPUT ki; + [FieldOffset(0)] + internal HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct MOUSEINPUT + { + internal int dx; + internal int dy; + internal int mouseData; + internal uint dwFlags; + internal uint time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct KEYBDINPUT + { + internal short wVk; + internal short wScan; + internal uint dwFlags; + internal int time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct HARDWAREINPUT + { + internal int uMsg; + internal short wParamL; + internal short wParamH; + } + + internal enum INPUTTYPE : uint + { + INPUT_MOUSE = 0, + INPUT_KEYBOARD = 1, + INPUT_HARDWARE = 2, + } + + [Flags] + internal enum KeyEventF + { + KeyDown = 0x0000, + ExtendedKey = 0x0001, + KeyUp = 0x0002, + Unicode = 0x0004, + Scancode = 0x0008, + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeMethods.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeMethods.cs new file mode 100644 index 0000000000..f89b11391b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/NativeMethods.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.PowerToys.Settings.UI.Helpers; + +public static class NativeMethods +{ + private const int WS_POPUP = 1 << 31; // 0x80000000 + internal const int GWL_STYLE = -16; + internal const int WS_CAPTION = 0x00C00000; + internal const int SPI_GETDESKWALLPAPER = 0x0073; + internal const int SW_SHOWNORMAL = 1; + internal const int SW_SHOWMAXIMIZED = 3; + internal const int SW_HIDE = 0; + + [DllImport("user32.dll")] + internal static extern IntPtr GetActiveWindow(); + + [DllImport("user32.dll")] + internal static extern bool SetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll")] + internal static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll")] + internal static extern uint SendInput(uint nInputs, NativeKeyboardHelper.INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + internal static extern short GetAsyncKeyState(int vKey); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + // [DllImport("shell32.dll")] + // internal static extern IntPtr SHBrowseForFolderW(ref ShellGetFolder.BrowseInformation browseInfo); + [DllImport("shell32.dll")] + internal static extern int SHGetPathFromIDListW(IntPtr pidl, IntPtr pszPath); + + // [DllImport("Comdlg32.dll", CharSet = CharSet.Unicode)] + // internal static extern bool GetOpenFileName([In, Out] OpenFileName openFileName); +#pragma warning disable CA1401 // P/Invokes should not be visible + [DllImport("user32.dll")] + public static extern bool ShowWindow(System.IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + public static extern int GetDpiForWindow(System.IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool AllowSetForegroundWindow(int dwProcessId); + + [System.Runtime.InteropServices.DllImport("User32.dll")] + public static extern bool SetForegroundWindow(IntPtr handle); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr LoadLibrary(string dllToLoad); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern bool FreeLibrary(IntPtr hModule); + +#pragma warning restore CA1401 // P/Invokes should not be visible + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + + internal static extern bool SystemParametersInfo(int uiAction, int uiParam, StringBuilder pvParam, int fWinIni); + + public static void SetPopupStyle(IntPtr hwnd) + { + _ = SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) | WS_POPUP); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/POINT.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/POINT.cs new file mode 100644 index 0000000000..e1274282c9 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/POINT.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X { get; set; } + + public int Y { get; set; } + + public POINT(int x, int y) + { + X = x; + Y = y; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/RECT.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/RECT.cs new file mode 100644 index 0000000000..b35af77254 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/RECT.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left { get; set; } + + public int Top { get; set; } + + public int Right { get; set; } + + public int Bottom { get; set; } + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml new file mode 100644 index 0000000000..f470c54791 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml @@ -0,0 +1,53 @@ + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml.cs new file mode 100644 index 0000000000..b89a627d70 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -0,0 +1,511 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; +using Microsoft.CmdPal.UI.Library; +using Microsoft.CmdPal.UI.ViewModels.Settings; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Windows.System; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipient +{ + private readonly UIntPtr ignoreKeyEventFlag = 0x5555; + private readonly System.Collections.Generic.HashSet _modifierKeysOnEntering = new(); + private bool _enabled; + private HotkeySettings? hotkeySettings; + private HotkeySettings internalSettings; + private HotkeySettings? lastValidSettings; + private HotkeySettingsControlHook? hook; + private bool _isActive; + private bool disposedValue; + + public string Header { get; set; } = string.Empty; + + public string Keys { get; set; } = string.Empty; + + public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null); + public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null); + + public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged)); + + private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs? e) + { + var me = d as ShortcutControl; + if (me == null) + { + return; + } + + var description = me.c?.FindDescendant(); + if (description == null) + { + return; + } + + var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader; + var newValue = (bool)(e?.NewValue ?? false); + var text = newValue ? + resourceLoader.GetString("Activation_Shortcut_With_Disable_Description") : + resourceLoader.GetString("Activation_Shortcut_Description"); + description.Text = text; + } + + private readonly ShortcutDialogContentControl c = new(); + private readonly ContentDialog shortcutDialog; + + public bool AllowDisable + { + get => (bool)GetValue(AllowDisableProperty); + set => SetValue(AllowDisableProperty, value); + } + + public bool Enabled + { + get + { + return _enabled; + } + + set + { + SetValue(IsActiveProperty, value); + _enabled = value; + + EditButton.IsEnabled = value; + } + } + + public HotkeySettings? HotkeySettings + { + get + { + return hotkeySettings; + } + + set + { + if (hotkeySettings != value) + { + hotkeySettings = value; + SetValue(HotkeySettingsProperty, value); + PreviewKeysControl.ItemsSource = HotkeySettings?.GetKeysList() ?? new List(); + AutomationProperties.SetHelpText(EditButton, HotkeySettings?.ToString() ?? string.Empty); + c.Keys = HotkeySettings?.GetKeysList() ?? new List(); + } + } + } + + public ShortcutControl() + { + InitializeComponent(); + internalSettings = new HotkeySettings(); + + var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader; + + // We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme. + shortcutDialog = new ContentDialog + { + XamlRoot = this.XamlRoot, + Title = resourceLoader.GetString("Activation_Shortcut_Title"), + Content = c, + PrimaryButtonText = resourceLoader.GetString("Activation_Shortcut_Save"), + SecondaryButtonText = resourceLoader.GetString("Activation_Shortcut_Reset"), + CloseButtonText = resourceLoader.GetString("Activation_Shortcut_Cancel"), + DefaultButton = ContentDialogButton.Primary, + }; + shortcutDialog.SecondaryButtonClick += ShortcutDialog_Reset; + shortcutDialog.RightTapped += ShortcutDialog_Disable; + + // The original ShortcutControl from PowerToys would hook up the bodies + // of DoLoad and DoUnload as `Loaded` and `Unloaded` handlers for `this`. + // We can't do that - since we might be virtualized in a list / + // ItemsRepeater, where those events are weirdly busted. We'd get both + // a Loaded and Unloaded as soon as we're displayed, which won't do. + // + // Instead, we'll do the work they used to do on Load/Unload when the + // dialog for this control is Opened/Close, respectively. + shortcutDialog.Opened += (s, e) => DoLoad(); + shortcutDialog.Closed += (s, e) => DoUnload(); + shortcutDialog.Opened += ShortcutDialog_Opened; + shortcutDialog.Closing += ShortcutDialog_Closing; + + AutomationProperties.SetName(EditButton, resourceLoader.GetString("Activation_Shortcut_Title")); + + WeakReferenceMessenger.Default.Register(this); + + OnAllowDisableChanged(this, null); + } + + private void DoUnload() + { + shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick; + + // The original version of this control in PowerToys would add an event + // handler to the AppWindow here, to track if the window was active or + // inactive. + // + // That doesn't really work in our setup, as we might have multiple + // AppWindows per instance. Instead, we're having the SettingsWindow + // send us the WindowActivatedEventArgs, so that we can know when to + // stop our hook thread + + // Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded + hook?.Dispose(); + + hook = null; + } + + private void DoLoad() + { + // These all belong here; because of virtualization in e.g. a ListView, the control can go through several Loaded / Unloaded cycles. + hook?.Dispose(); + + hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents); + + shortcutDialog.PrimaryButtonClick += ShortcutDialog_PrimaryButtonClick; + } + + private void KeyEventHandler(int key, bool matchValue, int matchValueCode) + { + var virtualKey = (VirtualKey)key; + switch (virtualKey) + { + case VirtualKey.LeftWindows: + case VirtualKey.RightWindows: + if (!matchValue && _modifierKeysOnEntering.Contains(virtualKey)) + { + SendSingleKeyboardInput((short)virtualKey, (uint)NativeKeyboardHelper.KeyEventF.KeyUp); + _ = _modifierKeysOnEntering.Remove(virtualKey); + } + + internalSettings.Win = matchValue; + break; + case VirtualKey.Control: + case VirtualKey.LeftControl: + case VirtualKey.RightControl: + if (!matchValue && _modifierKeysOnEntering.Contains(VirtualKey.Control)) + { + SendSingleKeyboardInput((short)virtualKey, (uint)NativeKeyboardHelper.KeyEventF.KeyUp); + _ = _modifierKeysOnEntering.Remove(VirtualKey.Control); + } + + internalSettings.Ctrl = matchValue; + break; + case VirtualKey.Menu: + case VirtualKey.LeftMenu: + case VirtualKey.RightMenu: + if (!matchValue && _modifierKeysOnEntering.Contains(VirtualKey.Menu)) + { + SendSingleKeyboardInput((short)virtualKey, (uint)NativeKeyboardHelper.KeyEventF.KeyUp); + _ = _modifierKeysOnEntering.Remove(VirtualKey.Menu); + } + + internalSettings.Alt = matchValue; + break; + case VirtualKey.Shift: + case VirtualKey.LeftShift: + case VirtualKey.RightShift: + if (!matchValue && _modifierKeysOnEntering.Contains(VirtualKey.Shift)) + { + SendSingleKeyboardInput((short)virtualKey, (uint)NativeKeyboardHelper.KeyEventF.KeyUp); + _ = _modifierKeysOnEntering.Remove(VirtualKey.Shift); + } + + internalSettings.Shift = matchValue; + break; + case VirtualKey.Escape: + internalSettings = new HotkeySettings(); + shortcutDialog.IsPrimaryButtonEnabled = false; + return; + default: + internalSettings.Code = matchValueCode; + break; + } + } + + // Function to send a single key event to the system which would be ignored by the hotkey control. + private void SendSingleKeyboardInput(short keyCode, uint keyStatus) + { + var inputShift = new NativeKeyboardHelper.INPUT + { + type = NativeKeyboardHelper.INPUTTYPE.INPUT_KEYBOARD, + data = new NativeKeyboardHelper.InputUnion + { + ki = new NativeKeyboardHelper.KEYBDINPUT + { + wVk = keyCode, + dwFlags = keyStatus, + + // Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead. + dwExtraInfo = ignoreKeyEventFlag, + }, + }, + }; + + NativeKeyboardHelper.INPUT[] inputs = [inputShift]; + + _ = NativeMethods.SendInput(1, inputs, NativeKeyboardHelper.INPUT.Size); + } + + private bool FilterAccessibleKeyboardEvents(int key, UIntPtr extraInfo) + { + // A keyboard event sent with this value in the extra Information field should be ignored by the hook so that it can be captured by the system instead. + if (extraInfo == ignoreKeyEventFlag) + { + return false; + } + + // If the current key press is tab, based on the other keys ignore the key press so as to shift focus out of the hotkey control. + if ((VirtualKey)key == VirtualKey.Tab) + { + // Shift was not pressed while entering and Shift is not pressed while leaving the hotkey control, treat it as a normal tab key press. + if (!internalSettings.Shift && !_modifierKeysOnEntering.Contains(VirtualKey.Shift) && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl) + { + return false; + } + + // Shift was not pressed while entering but it was pressed while leaving the hotkey, therefore simulate a shift key press as the system does not know about shift being pressed in the hotkey. + else if (internalSettings.Shift && !_modifierKeysOnEntering.Contains(VirtualKey.Shift) && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl) + { + // This is to reset the shift key press within the control as it was not used within the control but rather was used to leave the hotkey. + internalSettings.Shift = false; + + SendSingleKeyboardInput((short)VirtualKey.Shift, (uint)NativeKeyboardHelper.KeyEventF.KeyDown); + + return false; + } + + // Shift was pressed on entering and remained pressed, therefore only ignore the tab key so that it can be passed to the system. + // As the shift key is already assumed to be pressed by the system while it entered the hotkey control, shift would still remain pressed, hence ignoring the tab input would simulate a Shift+Tab key press. + else if (!internalSettings.Shift && _modifierKeysOnEntering.Contains(VirtualKey.Shift) && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl) + { + return false; + } + } + + // Either the cancel or save button has keyboard focus. + return FocusManager.GetFocusedElement(LayoutRoot.XamlRoot).GetType() != typeof(Button); + } + + private void Hotkey_KeyDown(int key) + { + KeyEventHandler(key, true, key); + + c.Keys = internalSettings.GetKeysList(); + + if (internalSettings.GetKeysList().Count == 0) + { + // Empty, disable save button + shortcutDialog.IsPrimaryButtonEnabled = false; + } + else if (internalSettings.GetKeysList().Count == 1) + { + // 1 key, disable save button + shortcutDialog.IsPrimaryButtonEnabled = false; + + // Check if the one key is a hotkey + c.IsError = !internalSettings.Shift && !internalSettings.Win && !internalSettings.Alt && !internalSettings.Ctrl; + } + + // Tab and Shift+Tab are accessible keys and should not be displayed in the hotkey control. + if (internalSettings.Code > 0 && !internalSettings.IsAccessibleShortcut()) + { + lastValidSettings = internalSettings with { }; + + if (!ComboIsValid(lastValidSettings)) + { + DisableKeys(); + } + else + { + EnableKeys(); + } + } + + c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0); + } + + private void EnableKeys() + { + shortcutDialog.IsPrimaryButtonEnabled = true; + c.IsError = false; + + // WarningLabel.Style = (Style)App.Current.Resources["SecondaryTextStyle"]; + } + + private void DisableKeys() + { + shortcutDialog.IsPrimaryButtonEnabled = false; + c.IsError = true; + + // WarningLabel.Style = (Style)App.Current.Resources["SecondaryWarningTextStyle"]; + } + + private void Hotkey_KeyUp(int key) + { + KeyEventHandler(key, false, 0); + } + + private bool Hotkey_IsActive() + { + return _isActive; + } + + private void ShortcutDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args) + { + if (!ComboIsValid(hotkeySettings)) + { + DisableKeys(); + } + else + { + EnableKeys(); + } + + // Reset the status on entering the hotkey each time. + _modifierKeysOnEntering.Clear(); + + // To keep track of the modifier keys, whether it was pressed on entering. + if ((NativeMethods.GetAsyncKeyState((int)VirtualKey.Shift) & 0x8000) != 0) + { + _modifierKeysOnEntering.Add(VirtualKey.Shift); + } + + if ((NativeMethods.GetAsyncKeyState((int)VirtualKey.Control) & 0x8000) != 0) + { + _modifierKeysOnEntering.Add(VirtualKey.Control); + } + + if ((NativeMethods.GetAsyncKeyState((int)VirtualKey.Menu) & 0x8000) != 0) + { + _modifierKeysOnEntering.Add(VirtualKey.Menu); + } + + if ((NativeMethods.GetAsyncKeyState((int)VirtualKey.LeftWindows) & 0x8000) != 0) + { + _modifierKeysOnEntering.Add(VirtualKey.LeftWindows); + } + + if ((NativeMethods.GetAsyncKeyState((int)VirtualKey.RightWindows) & 0x8000) != 0) + { + _modifierKeysOnEntering.Add(VirtualKey.RightWindows); + } + + _isActive = true; + } + + private async void OpenDialogButton_Click(object sender, RoutedEventArgs e) + { + // c.Keys = null; + c.Keys = HotkeySettings?.GetKeysList() ?? new List(); + + // 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win. + // Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore + c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92); + + shortcutDialog.XamlRoot = this.XamlRoot; + shortcutDialog.RequestedTheme = this.ActualTheme; + await shortcutDialog.ShowAsync(); + } + + private void ShortcutDialog_Reset(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + hotkeySettings = null; + + SetValue(HotkeySettingsProperty, hotkeySettings); + PreviewKeysControl.ItemsSource = HotkeySettings?.GetKeysList() ?? new List(); + + lastValidSettings = hotkeySettings; + + AutomationProperties.SetHelpText(EditButton, HotkeySettings?.ToString() ?? string.Empty); + shortcutDialog.Hide(); + } + + private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (lastValidSettings != null && ComboIsValid(lastValidSettings)) + { + HotkeySettings = lastValidSettings with { }; + } + + PreviewKeysControl.ItemsSource = hotkeySettings?.GetKeysList() ?? new List(); + AutomationProperties.SetHelpText(EditButton, HotkeySettings?.ToString() ?? string.Empty); + shortcutDialog.Hide(); + } + + private void ShortcutDialog_Disable(object sender, RightTappedRoutedEventArgs e) + { + if (!AllowDisable) + { + return; + } + + var empty = new HotkeySettings(); + HotkeySettings = empty; + + PreviewKeysControl.ItemsSource = HotkeySettings.GetKeysList(); + AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); + shortcutDialog.Hide(); + } + + private static bool ComboIsValid(HotkeySettings? settings) + { + return settings != null && (settings.IsValid() || settings.IsEmpty()); + } + + public void Receive(WindowActivatedEventArgs message) => DoWindowActivated(message); + + private void DoWindowActivated(WindowActivatedEventArgs args) + { + args.Handled = true; + if (args.WindowActivationState != WindowActivationState.Deactivated && (hook == null || hook.GetDisposedState() == true)) + { + // If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input. + hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents); + } + else if (args.WindowActivationState == WindowActivationState.Deactivated && hook != null && hook.GetDisposedState() == false) + { + // If the PT settings window lost focus/activation, we disable the keyboard hook to allow keyboard input on other windows. + hook.Dispose(); + hook = null; + } + } + + private void ShortcutDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args) + { + _isActive = false; + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + if (hook != null) + { + hook.Dispose(); + } + + hook = null; + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml new file mode 100644 index 0000000000..7932a27e91 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs new file mode 100644 index 0000000000..6db18e1676 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs @@ -0,0 +1,41 @@ +// 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.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ShortcutDialogContentControl : UserControl +{ + public ShortcutDialogContentControl() + { + this.InitializeComponent(); + } + + public List Keys + { + get { return (List)GetValue(KeysProperty); } + set { SetValue(KeysProperty, value); } + } + + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string))); + + public bool IsError + { + get => (bool)GetValue(IsErrorProperty); + set => SetValue(IsErrorProperty, value); + } + + public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + + public bool IsWarningAltGr + { + get => (bool)GetValue(IsWarningAltGrProperty); + set => SetValue(IsWarningAltGrProperty, value); + } + + public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml new file mode 100644 index 0000000000..e7bc518dee --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs new file mode 100644 index 0000000000..5605d5b195 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Controls +{ + public sealed partial class ShortcutWithTextLabelControl : UserControl + { + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + + public List Keys + { + get { return (List)GetValue(KeysProperty); } + set { SetValue(KeysProperty, value); } + } + + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + + public ShortcutWithTextLabelControl() + { + this.InitializeComponent(); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/WINDOWPLACEMENT.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/WINDOWPLACEMENT.cs new file mode 100644 index 0000000000..4b9189899e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/WINDOWPLACEMENT.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWPLACEMENT + { + public int Length { get; set; } + + public int Flags { get; set; } + + public int ShowCmd { get; set; } + + public POINT MinPosition { get; set; } + + public POINT MaxPosition { get; set; } + + public RECT NormalPosition { get; set; } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SourceRequestedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SourceRequestedEventArgs.cs new file mode 100644 index 0000000000..670bf13a7a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SourceRequestedEventArgs.cs @@ -0,0 +1,21 @@ +// 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 CommunityToolkit.Common.Deferred; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Controls; + +/// +/// See event. +/// +public class SourceRequestedEventArgs(object? key, ElementTheme requestedTheme) : DeferredEventArgs +{ + public object? Key { get; private set; } = key; + + public IconSource? Value { get; set; } + + public ElementTheme Theme => requestedTheme; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml new file mode 100644 index 0000000000..7b783c024c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + 4,2,4,2 + 1 + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml.cs new file mode 100644 index 0000000000..ec1be70904 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml.cs @@ -0,0 +1,153 @@ +// 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.UI.Helpers; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CommandPalette.Extensions; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace Microsoft.CmdPal.UI.Controls; + +[TemplatePart(Name = TagIconBox, Type = typeof(IconBox))] + +public partial class Tag : Control +{ + internal const string TagIconBox = "PART_Icon"; + + public OptionalColor? BackgroundColor + { + get => (OptionalColor?)GetValue(BackgroundColorProperty); + set => SetValue(BackgroundColorProperty, value); + } + + public OptionalColor? ForegroundColor + { + get => (OptionalColor?)GetValue(ForegroundColorProperty); + set => SetValue(ForegroundColorProperty, value); + } + + public bool HasIcon => Icon?.HasIcon(this.ActualTheme == ElementTheme.Light) ?? false; + + public IconInfoViewModel? Icon + { + get => (IconInfoViewModel?)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public string? Text + { + get => (string?)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + private static Brush? OriginalBg => Application.Current.Resources["TagBackground"] as Brush; + + private static Brush? OriginalFg => Application.Current.Resources["TagForeground"] as Brush; + + private static Brush? OriginalBorder => Application.Current.Resources["TagBorderBrush"] as Brush; + + public static readonly DependencyProperty ForegroundColorProperty = + DependencyProperty.Register(nameof(ForegroundColor), typeof(OptionalColor), typeof(Tag), new PropertyMetadata(null, OnForegroundColorPropertyChanged)); + + public static readonly DependencyProperty BackgroundColorProperty = + DependencyProperty.Register(nameof(BackgroundColor), typeof(OptionalColor), typeof(Tag), new PropertyMetadata(null, OnBackgroundColorPropertyChanged)); + + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register(nameof(Icon), typeof(IconInfoViewModel), typeof(Tag), new PropertyMetadata(null)); + + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register(nameof(Text), typeof(string), typeof(Tag), new PropertyMetadata(null)); + + public Tag() + { + this.DefaultStyleKey = typeof(Tag); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (GetTemplateChild(TagIconBox) is IconBox iconBox) + { + iconBox.SourceRequested += IconCacheProvider.SourceRequested; + iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed; + } + } + + private static void OnForegroundColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not Tag tag) + { + return; + } + + if (tag.ForegroundColor != null && + OptionalColorBrushCacheProvider.Convert(tag.ForegroundColor.Value) is SolidColorBrush brush) + { + tag.Foreground = brush; + + // If we have a BG color, then don't apply a border. + if (tag.BackgroundColor is OptionalColor bg && bg.HasValue) + { + tag.BorderBrush = OriginalBorder; + } + else + { + // Otherwise (no background), use the FG as the border + tag.BorderBrush = brush; + } + } + else + { + tag.Foreground = OriginalFg; + tag.BorderBrush = OriginalBorder; + } + } + + private static void OnBackgroundColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not Tag tag) + { + return; + } + + if (tag.BackgroundColor != null && + OptionalColorBrushCacheProvider.Convert(tag.BackgroundColor.Value) is SolidColorBrush brush) + { + tag.Background = brush; + + // Since we have a BG here, we never want a border. + tag.BorderBrush = OriginalBorder; + + // If we have a FG color, then don't apply a border. + if (tag.ForegroundColor is OptionalColor fg && fg.HasValue) + { + tag.BorderBrush = OriginalBorder; + } + else + { + // Otherwise (no foreground), use the FG as the border + tag.BorderBrush = brush; + } + } + else + { + // No BG color here. + tag.Background = OriginalBg; + + // If we have a FG color, then don't apply a border. + if (tag.ForegroundColor is OptionalColor fg && fg.HasValue) + { + tag.BorderBrush = tag.Foreground; + } + else + { + // Otherwise (no foreground), use the FG as the border + tag.BorderBrush = OriginalBorder; + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContentTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContentTemplateSelector.cs new file mode 100644 index 0000000000..ddcf4e0de5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContentTemplateSelector.cs @@ -0,0 +1,33 @@ +// 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.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI; + +public partial class ContentTemplateSelector : DataTemplateSelector +{ + // Define the (currently empty) data templates to return + // These will be "filled-in" in the XAML code. + public DataTemplate? FormTemplate { get; set; } + + public DataTemplate? MarkdownTemplate { get; set; } + + public DataTemplate? TreeTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + return item is ContentViewModel element + ? element switch + { + ContentFormViewModel => FormTemplate, + ContentMarkdownViewModel => MarkdownTemplate, + ContentTreeViewModel => TreeTemplate, + _ => null, + } + : null; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/DetailsDataTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/DetailsDataTemplateSelector.cs new file mode 100644 index 0000000000..328e0ca01e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/DetailsDataTemplateSelector.cs @@ -0,0 +1,37 @@ +// 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.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI; + +public partial class DetailsDataTemplateSelector : DataTemplateSelector +{ + // Define the (currently empty) data templates to return + // These will be "filled-in" in the XAML code. + public DataTemplate? LinkTemplate { get; set; } + + public DataTemplate? SeparatorTemplate { get; set; } + + public DataTemplate? TagTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + if (item is DetailsElementViewModel element) + { + var data = element; + return data switch + { + DetailsSeparatorViewModel => SeparatorTemplate, + DetailsLinkViewModel => LinkTemplate, + DetailsTagsViewModel => TagTemplate, + _ => null, + }; + } + + return null; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/KeyChordToStringConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/KeyChordToStringConverter.cs new file mode 100644 index 0000000000..9b22dd01e1 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/KeyChordToStringConverter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.UI.Xaml.Data; +using Windows.System; +using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; + +namespace Microsoft.CmdPal.UI; + +public partial class KeyChordToStringConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is KeyChord shortcut && (VirtualKey)shortcut.Vkey != VirtualKey.None) + { + var result = string.Empty; + + if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Control)) + { + result += "Ctrl+"; + } + + if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Shift)) + { + result += "Shift+"; + } + + if (shortcut.Modifiers.HasFlag(VirtualKeyModifiers.Menu)) + { + result += "Alt+"; + } + + result += (VirtualKey)shortcut.Vkey; + + return result; + } + + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/MessageStateToSeverityConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/MessageStateToSeverityConverter.cs new file mode 100644 index 0000000000..b5b87a07ec --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/MessageStateToSeverityConverter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.CmdPal.UI; + +public partial class MessageStateToSeverityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is MessageState state) + { + switch (state) + { + case MessageState.Info: + return InfoBarSeverity.Informational; + case MessageState.Success: + return InfoBarSeverity.Success; + case MessageState.Warning: + return InfoBarSeverity.Warning; + case MessageState.Error: + return InfoBarSeverity.Error; + } + } + + return InfoBarSeverity.Informational; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/PlaceholderTextConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/PlaceholderTextConverter.cs new file mode 100644 index 0000000000..deb8cca444 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/PlaceholderTextConverter.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml.Data; +using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; + +namespace Microsoft.CmdPal.UI; + +public partial class PlaceholderTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + return value is string placeholder && !string.IsNullOrEmpty(placeholder) + ? placeholder + : (object)RS_.GetString("DefaultSearchPlaceholderText"); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/CleanupHelper.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/CleanupHelper.xaml.cs new file mode 100644 index 0000000000..3436e46481 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/CleanupHelper.xaml.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.CmdPal.UI; + +public static class CleanupHelper +{ + public static void Cleanup(FrameworkElement element) + { + var count = VisualTreeHelper.GetChildrenCount(element); + for (var index = 0; index < count; index++) + { + var child = VisualTreeHelper.GetChild(element, index); + if (child is FrameworkElement childElement) + { + Cleanup(childElement); + } + } + + switch (element) + { + case ItemsControl itemsControl: + itemsControl.ItemsSource = null; + break; + case ItemsRepeater itemsRepeater: + itemsRepeater.ItemsSource = null; + break; + case TabView tabView: + tabView.TabItemsSource = null; + break; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml new file mode 100644 index 0000000000..8cc1174a1b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs new file mode 100644 index 0000000000..1b53a41339 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ContentPage.xaml.cs @@ -0,0 +1,72 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.CmdPal.UI; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ContentPage : Page, + IRecipient, + IRecipient +{ + private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); + + public ContentPageViewModel? ViewModel + { + get => (ContentPageViewModel?)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + // Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register(nameof(ViewModel), typeof(ContentPageViewModel), typeof(ContentPage), new PropertyMetadata(null)); + + public ContentPage() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (e.Parameter is ContentPageViewModel vm) + { + ViewModel = vm; + } + + base.OnNavigatedTo(e); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + + // Clean-up event listeners + ViewModel = null; + } + + // this comes in on Enter keypresses in the SearchBox + public void Receive(ActivateSelectedListItemMessage message) + { + ViewModel?.InvokePrimaryCommandCommand?.Execute(ViewModel); + } + + // this comes in on Ctrl+Enter keypresses in the SearchBox + public void Receive(ActivateSecondaryCommandMessage message) + { + ViewModel?.InvokeSecondaryCommandCommand?.Execute(ViewModel); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml new file mode 100644 index 0000000000..28537c1b78 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs new file mode 100644 index 0000000000..9e4832abf1 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -0,0 +1,291 @@ +// 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; +using CommunityToolkit.Mvvm.Messaging; +using ManagedCommon; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.CmdPal.UI; + +public sealed partial class ListPage : Page, + IRecipient, + IRecipient, + IRecipient, + IRecipient +{ + private ListViewModel? ViewModel + { + get => (ListViewModel?)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + // Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged)); + + public ListPage() + { + this.InitializeComponent(); + this.NavigationCacheMode = NavigationCacheMode.Disabled; + this.ItemsList.Loaded += ItemsList_Loaded; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (e.Parameter is ListViewModel lvm) + { + ViewModel = lvm; + } + + if (e.NavigationMode == NavigationMode.Back + || (e.NavigationMode == NavigationMode.New && ItemsList.Items.Count > 0)) + { + // Upon navigating _back_ to this page, immediately select the + // first item in the list + ItemsList.SelectedIndex = 0; + } + + // RegisterAll isn't AOT compatible + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + base.OnNavigatedTo(e); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + + if (ViewModel != null) + { + ViewModel.PropertyChanged -= ViewModel_PropertyChanged; + ViewModel.ItemsUpdated -= Page_ItemsUpdated; + } + + if (e.NavigationMode != NavigationMode.New) + { + ViewModel?.SafeCleanup(); + CleanupHelper.Cleanup(this); + Bindings.StopTracking(); + } + + // Clean-up event listeners + ViewModel = null; + + GC.Collect(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")] + private void ItemsList_ItemClick(object sender, ItemClickEventArgs e) + { + if (e.ClickedItem is ListItemViewModel item) + { + var settings = App.Current.Services.GetService()!; + if (settings.SingleClickActivates) + { + ViewModel?.InvokeItemCommand.Execute(item); + } + else + { + ViewModel?.UpdateSelectedItemCommand.Execute(item); + WeakReferenceMessenger.Default.Send(); + } + } + } + + private void ItemsList_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) + { + if (ItemsList.SelectedItem is ListItemViewModel vm) + { + var settings = App.Current.Services.GetService()!; + if (!settings.SingleClickActivates) + { + ViewModel?.InvokeItemCommand.Execute(vm); + } + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")] + private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ItemsList.SelectedItem is ListItemViewModel item) + { + var vm = ViewModel; + _ = Task.Run(() => + { + vm?.UpdateSelectedItemCommand.Execute(item); + }); + } + + // There's mysterious behavior here, where the selection seemingly + // changes to _nothing_ when we're backspacing to a single character. + // And at that point, seemingly the item that's getting removed is not + // a member of FilteredItems. Very bizarre. + // + // Might be able to fix in the future by stashing the removed item + // here, then in Page_ItemsUpdated trying to select that cached item if + // it's in the list (otherwise, clear the cache), but that seems + // aggressively BODGY for something that mostly just works today. + if (ItemsList.SelectedItem != null) + { + ItemsList.ScrollIntoView(ItemsList.SelectedItem); + } + } + + private void ItemsList_Loaded(object sender, RoutedEventArgs e) + { + // Find the ScrollViewer in the ListView + var listViewScrollViewer = FindScrollViewer(this.ItemsList); + + if (listViewScrollViewer != null) + { + listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged; + } + } + + private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) + { + var scrollView = sender as ScrollViewer; + if (scrollView == null) + { + return; + } + + // When we get to the bottom, request more from the extension, if they + // have more to give us. + // We're checking when we get to 80% of the scroll height, to give the + // extension a bit of a heads-up before the user actually gets there. + if (scrollView.VerticalOffset >= (scrollView.ScrollableHeight * .8)) + { + ViewModel?.LoadMoreIfNeeded(); + } + } + + public void Receive(NavigateNextCommand message) + { + // Note: We may want to just have the notion of a 'SelectedCommand' in our VM + // And then have these commands manipulate that state being bound to the UI instead + // We may want to see how other non-list UIs need to behave to make this decision + // At least it's decoupled from the SearchBox now :) + if (ItemsList.SelectedIndex < ItemsList.Items.Count - 1) + { + ItemsList.SelectedIndex++; + } + } + + public void Receive(NavigatePreviousCommand message) + { + if (ItemsList.SelectedIndex > 0) + { + ItemsList.SelectedIndex--; + } + } + + public void Receive(ActivateSelectedListItemMessage message) + { + if (ViewModel?.ShowEmptyContent ?? false) + { + ViewModel?.InvokeItemCommand.Execute(null); + } + else if (ItemsList.SelectedItem is ListItemViewModel item) + { + ViewModel?.InvokeItemCommand.Execute(item); + } + } + + public void Receive(ActivateSecondaryCommandMessage message) + { + if (ViewModel?.ShowEmptyContent ?? false) + { + ViewModel?.InvokeSecondaryCommandCommand.Execute(null); + } + else if (ItemsList.SelectedItem is ListItemViewModel item) + { + ViewModel?.InvokeSecondaryCommandCommand.Execute(item); + } + } + + private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ListPage @this) + { + if (e.OldValue is ListViewModel old) + { + old.PropertyChanged -= @this.ViewModel_PropertyChanged; + old.ItemsUpdated -= @this.Page_ItemsUpdated; + } + + if (e.NewValue is ListViewModel page) + { + page.PropertyChanged += @this.ViewModel_PropertyChanged; + page.ItemsUpdated += @this.Page_ItemsUpdated; + } + else if (e.NewValue == null) + { + Logger.LogDebug("cleared viewmodel"); + } + } + } + + // Called after we've finished updating the whole list for either a + // GetItems or a change in the filter. + private void Page_ItemsUpdated(ListViewModel sender, object args) + { + // If for some reason, we don't have a selected item, fix that. + // + // It's important to do this here, because once there's no selection + // (which can happen as the list updates) we won't get an + // ItemsList_SelectionChanged again to give us another chance to change + // the selection from null -> something. Better to just update the + // selection once, at the end of all the updating. + if (ItemsList.SelectedItem == null) + { + ItemsList.SelectedIndex = 0; + } + } + + private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var prop = e.PropertyName; + if (prop == nameof(ViewModel.FilteredItems)) + { + Debug.WriteLine($"ViewModel.FilteredItems {ItemsList.SelectedItem}"); + } + } + + private ScrollViewer? FindScrollViewer(DependencyObject parent) + { + if (parent is ScrollViewer) + { + return (ScrollViewer)parent; + } + + for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + var result = FindScrollViewer(child); + if (result != null) + { + return result; + } + } + + return null; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GpoValueChecker.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GpoValueChecker.cs new file mode 100644 index 0000000000..fcd8ba1590 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GpoValueChecker.cs @@ -0,0 +1,100 @@ +// 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.CmdPal.Ext.Bookmarks; +using Microsoft.UI.Xaml.Documents; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.UI.Helpers; + +internal enum GpoRuleConfiguredValue +{ + WrongValue = -3, + Unavailable = -2, + NotConfigured = -1, + Disabled = 0, + Enabled = 1, +} + +/* + * Contains methods extracted from PowerToys gpo.h + * The idea is to keep CmdPal codebase take as little dependences on the PowerToys codebase as possible. + * Having this class to check GPO being contained in CmdPal means we don't need to depend on GPOWrapper. + */ +internal static class GpoValueChecker +{ + private const string PoliciesPath = @"SOFTWARE\Policies\PowerToys"; + private static readonly RegistryKey PoliciesScopeMachine = Registry.LocalMachine; + private static readonly RegistryKey PoliciesScopeUser = Registry.CurrentUser; + private const string PolicyConfigureEnabledCmdPal = @"ConfigureEnabledUtilityCmdPal"; + private const string PolicyConfigureEnabledGlobalAllUtilities = @"ConfigureGlobalUtilityEnabledState"; + + private static GpoRuleConfiguredValue GetConfiguredValue(string registryValueName) + { + // For GPO policies, machine scope should take precedence over user scope + var value = ReadRegistryValue(PoliciesScopeMachine, PoliciesPath, registryValueName); + + if (!value.HasValue) + { + // If not found in machine scope, check user scope + value = ReadRegistryValue(PoliciesScopeUser, PoliciesPath, registryValueName); + if (!value.HasValue) + { + return GpoRuleConfiguredValue.NotConfigured; + } + } + + return value switch + { + 0 => GpoRuleConfiguredValue.Disabled, + 1 => GpoRuleConfiguredValue.Enabled, + _ => GpoRuleConfiguredValue.WrongValue, + }; + } + + // Reads an integer registry value if it exists. + private static int? ReadRegistryValue(RegistryKey rootKey, string subKeyPath, string valueName) + { + using (RegistryKey? key = rootKey.OpenSubKey(subKeyPath, false)) + { + if (key == null) + { + return null; + } + + var value = key.GetValue(valueName); + if (value is int intValue) + { + return intValue; + } + + return null; + } + } + + private static GpoRuleConfiguredValue GetUtilityEnabledValue(string utilityName) + { + var individualValue = GetConfiguredValue(utilityName); + + if (individualValue == GpoRuleConfiguredValue.Disabled || individualValue == GpoRuleConfiguredValue.Enabled) + { + return individualValue; + } + else + { + // If the individual utility value is not set, check the global all utilities policy value. + return GetConfiguredValue(PolicyConfigureEnabledGlobalAllUtilities); + } + } + + internal static GpoRuleConfiguredValue GetConfiguredCmdPalEnabledValue() + { + return GetUtilityEnabledValue(PolicyConfigureEnabledCmdPal); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheProvider.cs new file mode 100644 index 0000000000..01c9f05f4d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheProvider.cs @@ -0,0 +1,44 @@ +// 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.UI.Controls; +using Microsoft.CmdPal.UI.ViewModels; + +namespace Microsoft.CmdPal.UI.Helpers; + +/// +/// Common async event handler provides the cache lookup function for the deferred event. +/// +public static partial class IconCacheProvider +{ + private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); + +#pragma warning disable IDE0060 // Remove unused parameter + public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args) +#pragma warning restore IDE0060 // Remove unused parameter + { + if (args.Key == null) + { + return; + } + + if (args.Key is IconDataViewModel iconData) + { + var deferral = args.GetDeferral(); + + args.Value = await IconService.GetIconSource(iconData); + + deferral.Complete(); + } + else if (args.Key is IconInfoViewModel iconInfo) + { + var deferral = args.GetDeferral(); + + var data = args.Theme == Microsoft.UI.Xaml.ElementTheme.Dark ? iconInfo.Dark : iconInfo.Light; + args.Value = await IconService.GetIconSource(data); + + deferral.Complete(); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs new file mode 100644 index 0000000000..acf41fee1e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs @@ -0,0 +1,79 @@ +// 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; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.Terminal.UI; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.UI.Helpers; + +public sealed class IconCacheService(DispatcherQueue dispatcherQueue) +{ + public Task GetIconSource(IconDataViewModel icon) => + + // todo: actually implement a cache of some sort + IconToSource(icon); + + private async Task IconToSource(IconDataViewModel icon) + { + try + { + if (!string.IsNullOrEmpty(icon.Icon)) + { + var source = IconPathConverter.IconSourceMUX(icon.Icon, false); + return source; + } + else if (icon.Data != null) + { + try + { + return await StreamToIconSource(icon.Data.Unsafe!); + } + catch + { + Debug.WriteLine("Failed to load icon from stream"); + } + } + } + catch + { + } + + return null; + } + + private async Task StreamToIconSource(IRandomAccessStreamReference iconStreamRef) + { + if (iconStreamRef == null) + { + return null; + } + + var bitmap = await IconStreamToBitmapImageAsync(iconStreamRef); + var icon = new ImageIconSource() { ImageSource = bitmap }; + return icon; + } + + private async Task IconStreamToBitmapImageAsync(IRandomAccessStreamReference iconStreamRef) + { + // Return the bitmap image via TaskCompletionSource. Using WCT's EnqueueAsync does not suffice here, since if + // we're already on the thread of the DispatcherQueue then it just directly calls the function, with no async involved. + var completionSource = new TaskCompletionSource(); + dispatcherQueue.TryEnqueue(async () => + { + using var bitmapStream = await iconStreamRef.OpenReadAsync(); + var itemImage = new BitmapImage(); + await itemImage.SetSourceAsync(bitmapStream); + completionSource.TrySetResult(itemImage); + }); + + var bitmapImage = await completionSource.Task; + + return bitmapImage; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/OptionalColorToBrushConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/OptionalColorToBrushConverter.cs new file mode 100644 index 0000000000..7d12b0d770 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/OptionalColorToBrushConverter.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.UI; +using Microsoft.UI.Xaml.Media; +using Color = Windows.UI.Color; + +namespace Microsoft.CmdPal.UI.Helpers; + +public static partial class OptionalColorBrushCacheProvider +{ + private static readonly Dictionary _brushCache = []; + + public static SolidColorBrush? Convert(OptionalColor color) + { + if (!color.HasValue) + { + return null; + } + + if (!_brushCache.TryGetValue(color, out var brush)) + { + // Create and cache the brush if we see this color for the first time. + brush = new SolidColorBrush(Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)); + _brushCache[color] = brush; + } + + return brush; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ResourceLoaderInstance.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ResourceLoaderInstance.cs new file mode 100644 index 0000000000..08693f7a8f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ResourceLoaderInstance.cs @@ -0,0 +1,18 @@ +// 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.Windows.ApplicationModel.Resources; + +namespace Microsoft.CmdPal.UI.Helpers; + +internal static class ResourceLoaderInstance +{ + internal static ResourceLoader ResourceLoader { get; private set; } + + static ResourceLoaderInstance() + { + ResourceLoader = new ResourceLoader("resources.pri"); + } + + internal static string GetString(string resourceId) => ResourceLoader.GetString(resourceId); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs new file mode 100644 index 0000000000..561ad4592d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs @@ -0,0 +1,72 @@ +// 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 CommunityToolkit.Common.Deferred; +using Windows.Foundation; + +// Pilfered from CommunityToolkit.WinUI.Deferred +namespace Microsoft.CmdPal.UI.Deferred; + +/// +/// Extensions to for Deferred Events. +/// +public static class TypedEventHandlerExtensions +{ + /// + /// Use to invoke an async using . + /// + /// Type of sender. + /// type. + /// to be invoked. + /// Sender of the event. + /// instance. + /// to wait on deferred event handler. +#pragma warning disable CA1715 // Identifiers should have correct prefix +#pragma warning disable SA1314 // Type parameter names should begin with T + public static Task InvokeAsync(this TypedEventHandler eventHandler, S sender, R eventArgs) +#pragma warning restore SA1314 // Type parameter names should begin with T +#pragma warning restore CA1715 // Identifiers should have correct prefix + where R : DeferredEventArgs => InvokeAsync(eventHandler, sender, eventArgs, CancellationToken.None); + + /// + /// Use to invoke an async using with a . + /// + /// Type of sender. + /// type. + /// to be invoked. + /// Sender of the event. + /// instance. + /// option. + /// to wait on deferred event handler. +#pragma warning disable CA1715 // Identifiers should have correct prefix +#pragma warning disable SA1314 // Type parameter names should begin with T + public static Task InvokeAsync(this TypedEventHandler eventHandler, S sender, R eventArgs, CancellationToken cancellationToken) +#pragma warning restore SA1314 // Type parameter names should begin with T +#pragma warning restore CA1715 // Identifiers should have correct prefix + where R : DeferredEventArgs + { + if (eventHandler == null) + { + return Task.CompletedTask; + } + + var tasks = eventHandler.GetInvocationList() + .OfType>() + .Select(invocationDelegate => + { + cancellationToken.ThrowIfCancellationRequested(); + + invocationDelegate(sender, eventArgs); + +#pragma warning disable CS0618 // Type or member is obsolete + var deferral = eventArgs.GetCurrentDeferralAndReset(); + + return deferral?.WaitForCompletion(cancellationToken) ?? Task.CompletedTask; +#pragma warning restore CS0618 // Type or member is obsolete + }) + .ToArray(); + + return Task.WhenAll(tasks); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml new file mode 100644 index 0000000000..4e5e8259a3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml @@ -0,0 +1,13 @@ + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs new file mode 100644 index 0000000000..c09d1c9688 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Common.Helpers; +using Microsoft.CmdPal.Common.Messages; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Composition; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Input; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Windows.Foundation; +using Windows.Graphics; +using Windows.UI; +using Windows.UI.WindowManagement; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT; +using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; + +namespace Microsoft.CmdPal.UI; + +public sealed partial class MainWindow : Window, + IRecipient, + IRecipient, + IRecipient, + IRecipient +{ + private readonly HWND _hwnd; + private readonly WNDPROC? _hotkeyWndProc; + private readonly WNDPROC? _originalWndProc; + private readonly List _hotkeys = []; + + // Stylistically, window messages are WM_* +#pragma warning disable SA1310 // Field names should not contain underscore +#pragma warning disable SA1306 // Field names should begin with lower-case letter + private const uint MY_NOTIFY_ID = 1000; + private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1; + private readonly uint WM_TASKBAR_RESTART; +#pragma warning restore SA1306 // Field names should begin with lower-case letter +#pragma warning restore SA1310 // Field names should not contain underscore + + // Notification Area ("Tray") icon data + private NOTIFYICONDATAW? _trayIconData; + private bool _createdIcon; + private DestroyIconSafeHandle? _largeIcon; + + private DesktopAcrylicController? _acrylicController; + private SystemBackdropConfiguration? _configurationSource; + + public MainWindow() + { + InitializeComponent(); + + _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); + CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value); + + // TaskbarCreated is the message that's broadcast when explorer.exe + // restarts. We need to know when that happens to be able to bring our + // notification area icon back + WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated"); + + AppWindow.Title = RS_.GetString("AppName"); + AppWindow.Resize(new SizeInt32 { Width = 1000, Height = 620 }); + PositionCentered(); + SetAcrylic(); + + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + // Hide our titlebar. + // We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed + // to hide the old caption buttons. Then, in UpdateRegionsForCustomTitleBar, + // we'll make the top drag-able again. (after our content loads) + ExtendsContentIntoTitleBar = true; + AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed; + SizeChanged += WindowSizeChanged; + RootShellPage.Loaded += RootShellPage_Loaded; + + // LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a + // member (and instead like, use a local), then the pointer we marshal + // into the WindowLongPtr will be useless after we leave this function, + // and our **WindProc will explode**. + _hotkeyWndProc = HotKeyPrc; + var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc); + _originalWndProc = Marshal.GetDelegateForFunctionPointer(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer)); + AddNotificationIcon(); + + // Load our settings, and then also wire up a settings changed handler + HotReloadSettings(); + App.Current.Services.GetService()!.SettingsChanged += SettingsChangedHandler; + + // Make sure that we update the acrylic theme when the OS theme changes + RootShellPage.ActualThemeChanged += (s, e) => UpdateAcrylic(); + + // Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h + NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () => + { + Summon(string.Empty); + }); + } + + private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(); + + private void RootShellPage_Loaded(object sender, RoutedEventArgs e) => + + // Now that our content has loaded, we can update our draggable regions + UpdateRegionsForCustomTitleBar(); + + private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar(); + + private void PositionCentered() + { + var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest); + PositionCentered(displayArea); + } + + private void PositionCentered(DisplayArea displayArea) + { + if (displayArea is not null) + { + var centeredPosition = AppWindow.Position; + centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2; + centeredPosition.Y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2; + + centeredPosition.X += displayArea.WorkArea.X; + centeredPosition.Y += displayArea.WorkArea.Y; + AppWindow.Move(centeredPosition); + } + } + + private void HotReloadSettings() + { + var settings = App.Current.Services.GetService()!; + + SetupHotkey(settings); + + // This will prevent our window from appearing in alt+tab or the taskbar. + // You'll _need_ to use the hotkey to summon it. + AppWindow.IsShownInSwitchers = System.Diagnostics.Debugger.IsAttached; + } + + // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material + // other Shell surfaces are using, this cannot be set in XAML however. + private void SetAcrylic() + { + if (DesktopAcrylicController.IsSupported()) + { + // Hooking up the policy object. + _configurationSource = new SystemBackdropConfiguration + { + // Initial configuration state. + IsInputActive = true, + }; + UpdateAcrylic(); + } + } + + private void UpdateAcrylic() + { + _acrylicController = GetAcrylicConfig(Content); + + // Enable the system backdrop. + // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call. + _acrylicController.AddSystemBackdropTarget(this.As()); + _acrylicController.SetSystemBackdropConfiguration(_configurationSource); + } + + private static DesktopAcrylicController GetAcrylicConfig(UIElement content) + { + var feContent = content as FrameworkElement; + + return feContent?.ActualTheme == ElementTheme.Light + ? new DesktopAcrylicController() + { + Kind = DesktopAcrylicKind.Thin, + TintColor = Color.FromArgb(255, 243, 243, 243), + LuminosityOpacity = 0.90f, + TintOpacity = 0.0f, + FallbackColor = Color.FromArgb(255, 238, 238, 238), + } + : new DesktopAcrylicController() + { + Kind = DesktopAcrylicKind.Thin, + TintColor = Color.FromArgb(255, 32, 32, 32), + LuminosityOpacity = 0.96f, + TintOpacity = 0.5f, + FallbackColor = Color.FromArgb(255, 28, 28, 28), + }; + } + + private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) + { + var hwnd = new HWND(hwndValue); + + // Remember, IsIconic == "minimized", which is entirely different state + // from "show/hide" + // If we're currently minimized, restore us first, before we reveal + // our window. Otherwise we'd just be showing a minimized window - + // which would remain not visible to the user. + if (PInvoke.IsIconic(hwnd)) + { + PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE); + } + + var display = GetScreen(hwnd, target); + PositionCentered(display); + + PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW); + PInvoke.SetForegroundWindow(hwnd); + PInvoke.SetActiveWindow(hwnd); + } + + private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target) + { + // Leaving a note here, in case we ever need it: + // https://github.com/microsoft/microsoft-ui-xaml/issues/6454 + // If we need to ever FindAll, we'll need to iterate manually + var displayAreas = Microsoft.UI.Windowing.DisplayArea.FindAll(); + switch (target) + { + case MonitorBehavior.InPlace: + if (PInvoke.GetWindowRect(currentHwnd, out var bounds)) + { + RectInt32 converted = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); + return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest); + } + + break; + + case MonitorBehavior.ToFocusedWindow: + var foregroundWindowHandle = PInvoke.GetForegroundWindow(); + if (foregroundWindowHandle != IntPtr.Zero) + { + if (PInvoke.GetWindowRect(foregroundWindowHandle, out var fgBounds)) + { + RectInt32 converted = new(fgBounds.X, fgBounds.Y, fgBounds.Width, fgBounds.Height); + return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest); + } + } + + break; + + case MonitorBehavior.ToPrimary: + return DisplayArea.Primary; + + case MonitorBehavior.ToMouse: + default: + if (PInvoke.GetCursorPos(out var cursorPos)) + { + return DisplayArea.GetFromPoint(new PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.Nearest); + } + + break; + } + + return DisplayArea.Primary; + } + + public void Receive(ShowWindowMessage message) + { + var settings = App.Current.Services.GetService()!; + + ShowHwnd(message.Hwnd, settings.SummonOn); + } + + public void Receive(HideWindowMessage message) + { + PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE); + } + + public void Receive(QuitMessage message) + { + // This might come in on a background thread + DispatcherQueue.TryEnqueue(() => Close()); + } + + public void Receive(DismissMessage message) => + PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE); + + internal void MainWindow_Closed(object sender, WindowEventArgs args) + { + var serviceProvider = App.Current.Services; + var extensionService = serviceProvider.GetService()!; + extensionService.SignalStopExtensionsAsync(); + + RemoveNotificationIcon(); + + // WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592). + // Workaround by turning it off before shutdown. + App.Current.DebugSettings.FailFastOnErrors = false; + DisposeAcrylic(); + } + + private void DisposeAcrylic() + { + if (_acrylicController != null) + { + _acrylicController.Dispose(); + _acrylicController = null!; + _configurationSource = null!; + } + } + + // Updates our window s.t. the top of the window is draggable. + private void UpdateRegionsForCustomTitleBar() + { + // Specify the interactive regions of the title bar. + var scaleAdjustment = RootShellPage.XamlRoot.RasterizationScale; + + // Get the rectangle around our XAML content. We're going to mark this + // rectangle as "Passthrough", so that the normal window operations + // (resizing, dragging) don't apply in this space. + var transform = RootShellPage.TransformToVisual(null); + + // Reserve 16px of space at the top for dragging. + var topHeight = 16; + var bounds = transform.TransformBounds(new Rect( + 0, + topHeight, + RootShellPage.ActualWidth, + RootShellPage.ActualHeight)); + var contentRect = GetRect(bounds, scaleAdjustment); + var rectArray = new RectInt32[] { contentRect }; + var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id); + nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray); + + // Add a drag-able region on top + var w = RootShellPage.ActualWidth; + _ = RootShellPage.ActualHeight; + var dragSides = new RectInt32[] + { + GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall + }; + nonClientInputSrc.SetRegionRects(NonClientRegionKind.Caption, dragSides); + } + + private static RectInt32 GetRect(Rect bounds, double scale) + { + return new RectInt32( + _X: (int)Math.Round(bounds.X * scale), + _Y: (int)Math.Round(bounds.Y * scale), + _Width: (int)Math.Round(bounds.Width * scale), + _Height: (int)Math.Round(bounds.Height * scale)); + } + + internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args) + { + if (args.WindowActivationState == WindowActivationState.Deactivated) + { + // If there's a debugger attached... + if (System.Diagnostics.Debugger.IsAttached) + { + // ... then don't hide the window when it loses focus. + return; + } + else + { + PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE); + } + } + + if (_configurationSource != null) + { + _configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated; + } + } + + public void Summon(string commandId) + { + // The actual showing and hiding of the window will be done by the + // ShellPage. This is because we don't want to show the window if the + // user bound a hotkey to just an invokable command, which we can't + // know till the message is being handled. + WeakReferenceMessenger.Default.Send(new(commandId, _hwnd)); + } + +#pragma warning disable SA1310 // Field names should not contain underscore + private const uint DOT_KEY = 0xBE; + private const uint WM_HOTKEY = 0x0312; +#pragma warning restore SA1310 // Field names should not contain underscore + + private void UnregisterHotkeys() + { + while (_hotkeys.Count > 0) + { + PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1); + _hotkeys.RemoveAt(_hotkeys.Count - 1); + } + } + + private void SetupHotkey(SettingsModel settings) + { + UnregisterHotkeys(); + + var globalHotkey = settings.Hotkey; + if (globalHotkey != null) + { + var vk = globalHotkey.Code; + var modifiers = + (globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) | + (globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) | + (globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) | + (globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0) + ; + + var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk); + if (success) + { + _hotkeys.Add(new(globalHotkey, string.Empty)); + } + } + + foreach (var commandHotkey in settings.CommandHotkeys) + { + var key = commandHotkey.Hotkey; + + if (key != null) + { + var vk = key.Code; + var modifiers = + (key.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) | + (key.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) | + (key.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) | + (key.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0) + ; + + var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk); + if (success) + { + _hotkeys.Add(commandHotkey); + } + } + } + } + + private LRESULT HotKeyPrc( + HWND hwnd, + uint uMsg, + WPARAM wParam, + LPARAM lParam) + { + switch (uMsg) + { + case WM_HOTKEY: + { + var hotkeyIndex = (int)wParam.Value; + if (hotkeyIndex < _hotkeys.Count) + { + var hotkey = _hotkeys[hotkeyIndex]; + var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId); + + // Note to future us: the wParam will have the index of the hotkey we registered. + // We can use that in the future to differentiate the hotkeys we've pressed + // so that we can bind hotkeys to individual commands + if (!this.Visible || !isRootHotkey) + { + Summon(hotkey.CommandId); + } + else if (isRootHotkey) + { + PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE); + } + } + + return (LRESULT)IntPtr.Zero; + } + + // Shell_NotifyIcon can fail when we invoke it during the time explorer.exe isn't present/ready to handle it. + // We'll also never receive WM_TASKBAR_RESTART message if the first call to Shell_NotifyIcon failed, so we use + // WM_WINDOWPOSCHANGING which is always received on explorer startup sequence. + case PInvoke.WM_WINDOWPOSCHANGING: + { + if (!_createdIcon) + { + AddNotificationIcon(); + } + } + + break; + default: + // WM_TASKBAR_RESTART isn't a compile-time constant, so we can't + // use it in a case label + if (uMsg == WM_TASKBAR_RESTART) + { + // Handle the case where explorer.exe restarts. + // Even if we created it before, do it again + AddNotificationIcon(); + } + else if (uMsg == WM_TRAY_ICON) + { + switch ((uint)lParam.Value) + { + case PInvoke.WM_RBUTTONUP: + case PInvoke.WM_LBUTTONUP: + case PInvoke.WM_LBUTTONDBLCLK: + Summon(string.Empty); + break; + } + } + + break; + } + + return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam); + } + + private void AddNotificationIcon() + { + // We only need to build the tray data once. + if (_trayIconData == null) + { + // We need to stash this handle, so it doesn't clean itself up. If + // explorer restarts, we'll come back through here, and we don't + // really need to re-load the icon in that case. We can just use + // the handle from the first time. + _largeIcon = GetAppIconHandle(); + _trayIconData = new NOTIFYICONDATAW() + { + cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATAW)), + hWnd = _hwnd, + uID = MY_NOTIFY_ID, + uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP, + uCallbackMessage = WM_TRAY_ICON, + hIcon = (HICON)_largeIcon.DangerousGetHandle(), + szTip = RS_.GetString("AppStoreName"), + }; + } + + var d = (NOTIFYICONDATAW)_trayIconData; + + // Add the notification icon + if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d)) + { + _createdIcon = true; + } + } + + private void RemoveNotificationIcon() + { + if (_trayIconData != null && _createdIcon) + { + var d = (NOTIFYICONDATAW)_trayIconData; + if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d)) + { + _createdIcon = false; + } + } + } + + private DestroyIconSafeHandle GetAppIconHandle() + { + var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location; + DestroyIconSafeHandle largeIcon; + DestroyIconSafeHandle smallIcon; + PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out smallIcon, 1); + return largeIcon; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj new file mode 100644 index 0000000000..e6dba1da1a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -0,0 +1,197 @@ + + + + + + + + WinExe + Microsoft.CmdPal.UI + app.manifest + win-$(Platform).pubxml + true + true + enable + enable + + $(CmdPalVersion) + + + + false + false + + + + true + + + + + Microsoft.Terminal.UI + $(OutDir) + + + + + DISABLE_XAML_GENERATED_MAIN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + True + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + + + + + true + + + + + + + runtimes\win10-$(Platform)\native + + + + + + + + PreserveNewest + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt new file mode 100644 index 0000000000..6186b45ef8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt @@ -0,0 +1,36 @@ +GetPhysicallyInstalledSystemMemory +GlobalMemoryStatusEx +GetSystemInfo +CoCreateInstance +GetForegroundWindow +SetForegroundWindow +GetWindowRect +GetCursorPos +SetWindowPos +IsIconic +RegisterHotKey +UnregisterHotKey +SetWindowLongPtr +CallWindowProc +ShowWindow +SetForegroundWindow +EnableWindow +SetFocus +SetActiveWindow +MonitorFromWindow +GetMonitorInfo +SHCreateStreamOnFileEx +CoAllowSetForegroundWindow +SHCreateStreamOnFileEx +SHLoadIndirectString + +Shell_NotifyIcon +LoadIcon +WM_USER +WM_WINDOWPOSCHANGING +RegisterWindowMessageW +GetModuleHandleW +ExtractIconEx +WM_RBUTTONUP +WM_LBUTTONUP +WM_LBUTTONDBLCLK diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Package-Dev.appxmanifest b/src/modules/cmdpal/Microsoft.CmdPal.UI/Package-Dev.appxmanifest new file mode 100644 index 0000000000..b277026485 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Package-Dev.appxmanifest @@ -0,0 +1,81 @@ + + + + + + + + + + ms-resource:AppNameDev + A Lone Developer + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + com.microsoft.commandpalette + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Package.appxmanifest b/src/modules/cmdpal/Microsoft.CmdPal.UI/Package.appxmanifest new file mode 100644 index 0000000000..287db37a51 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Package.appxmanifest @@ -0,0 +1,81 @@ + + + + + + + + + + ms-resource:AppName + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + com.microsoft.commandpalette + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml new file mode 100644 index 0000000000..2d63992e8d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml.cs new file mode 100644 index 0000000000..66eff31c8a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/LoadingPage.xaml.cs @@ -0,0 +1,46 @@ +// 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.UI.ViewModels; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.CmdPal.UI.Pages; + +/// +/// We use this page to do initialization of our extensions and cache loading to hydrate our ViewModels. +/// +public sealed partial class LoadingPage : Page +{ + private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); + + public LoadingPage() + { + this.InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (e.Parameter is ShellViewModel shellVM + && shellVM.LoadCommand != null) + { + // 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) + { + // TODO: Handle failure case + } + }); + } + + base.OnNavigatedTo(e); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml new file mode 100644 index 0000000000..d5055074a0 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -0,0 +1,429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs new file mode 100644 index 0000000000..295bb5c1c4 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -0,0 +1,587 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; +using ManagedCommon; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.UI.Settings; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.MainPage; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; + +namespace Microsoft.CmdPal.UI.Pages; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + INotifyPropertyChanged +{ + private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); + + private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + + private readonly SlideNavigationTransitionInfo _slideRightTransition = new() { Effect = SlideNavigationTransitionEffect.FromRight }; + private readonly SuppressNavigationTransitionInfo _noAnimation = new(); + + private readonly ToastWindow _toast = new(); + + private readonly Lock _invokeLock = new(); + private Task? _handleInvokeTask; + + public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService()!; + + public event PropertyChangedEventHandler? PropertyChanged; + + public ShellPage() + { + this.InitializeComponent(); + + // how we are doing navigation around + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + + RootFrame.Navigate(typeof(LoadingPage), ViewModel); + } + + public void Receive(NavigateBackMessage message) + { + var settings = App.Current.Services.GetService()!; + + if (RootFrame.CanGoBack) + { + if (!message.FromBackspace || + settings.BackspaceGoesBack) + { + GoBack(); + } + } + else + { + if (!message.FromBackspace) + { + // If we can't go back then we must be at the top and thus escape again should quit. + WeakReferenceMessenger.Default.Send(); + } + } + } + + public void Receive(PerformCommandMessage message) + { + PerformCommand(message); + } + + private void PerformCommand(PerformCommandMessage message) + { + var command = message.Command.Unsafe; + if (command == null) + { + return; + } + + if (!ViewModel.CurrentPage.IsNested) + { + // on the main page here + ViewModel.PerformTopLevelCommand(message); + } + + IExtensionWrapper? extension = null; + + // TODO: Actually loading up the page, or invoking the command - + // that might belong in the model, not the view? + // Especially considering the try/catch concerns around the fact that the + // COM call might just fail. + // Or the command may be a stub. Future us problem. + try + { + var host = ViewModel.CurrentPage?.ExtensionHost ?? CommandPaletteHost.Instance; + + if (command is TopLevelCommandWrapper wrapper) + { + var tlc = wrapper; + command = wrapper.Command; + host = tlc.ExtensionHost != null ? tlc.ExtensionHost! : host; + extension = tlc.ExtensionHost?.Extension; + if (extension != null) + { + Logger.LogDebug($"Activated top-level command from {extension.ExtensionDisplayName}"); + } + } + + ViewModel.SetActiveExtension(extension); + + if (command is IPage page) + { + Logger.LogDebug($"Navigating to page"); + + // TODO GH #526 This needs more better locking too + _ = _queue.TryEnqueue(() => + { + // Also hide our details pane about here, if we had one + HideDetails(); + + WeakReferenceMessenger.Default.Send(new(null)); + + var isMainPage = command is MainListPage; + + // Construct our ViewModel of the appropriate type and pass it the UI Thread context. + PageViewModel pageViewModel = page switch + { + IListPage listPage => new ListViewModel(listPage, _mainTaskScheduler, host) + { + IsNested = !isMainPage, + }, + IContentPage contentPage => new ContentPageViewModel(contentPage, _mainTaskScheduler, host), + _ => throw new NotSupportedException(), + }; + + // Kick off async loading of our ViewModel + ViewModel.LoadPageViewModel(pageViewModel); + + // Navigate to the appropriate host page for that VM + RootFrame.Navigate( + page switch + { + IListPage => typeof(ListPage), + IContentPage => typeof(ContentPage), + _ => throw new NotSupportedException(), + }, + pageViewModel, + message.WithAnimation ? _slideRightTransition : _noAnimation); + + // Refocus on the Search for continual typing on the next search request + SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); + + if (isMainPage) + { + // todo BODGY + RootFrame.BackStack.Clear(); + } + + // 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. + }); + } + else if (command is IInvokableCommand invokable) + { + Logger.LogDebug($"Invoking command"); + HandleInvokeCommand(message, invokable); + } + } + catch (Exception ex) + { + // TODO: It would be better to do this as a page exception, rather + // than a silent log message. + CommandPaletteHost.Instance.Log(ex.Message); + } + } + + private void HandleInvokeCommand(PerformCommandMessage message, IInvokableCommand invokable) + { + // TODO GH #525 This needs more better locking. + lock (_invokeLock) + { + if (_handleInvokeTask != null) + { + // do nothing - a command is already doing a thing + } + else + { + _handleInvokeTask = Task.Run(() => + { + try + { + var result = invokable.Invoke(message.Context); + DispatcherQueue.TryEnqueue(() => + { + try + { + HandleCommandResultOnUiThread(result); + } + finally + { + _handleInvokeTask = null; + } + }); + } + catch (Exception ex) + { + _handleInvokeTask = null; + + // TODO: It would be better to do this as a page exception, rather + // than a silent log message. + CommandPaletteHost.Instance.Log(ex.Message); + } + }); + } + } + } + + // This gets called from the UI thread + private void HandleConfirmArgs(IConfirmationArgs args) + { + ConfirmResultViewModel vm = new(args, new(ViewModel.CurrentPage)); + var initializeDialogTask = Task.Run(() => { InitializeConfirmationDialog(vm); }); + initializeDialogTask.Wait(); + + var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader; + var confirmText = resourceLoader.GetString("ConfirmationDialog_ConfirmButtonText"); + var cancelText = resourceLoader.GetString("ConfirmationDialog_CancelButtonText"); + + var name = string.IsNullOrEmpty(vm.PrimaryCommand.Name) ? confirmText : vm.PrimaryCommand.Name; + ContentDialog dialog = new() + { + Title = vm.Title, + Content = vm.Description, + PrimaryButtonText = name, + CloseButtonText = cancelText, + XamlRoot = this.XamlRoot, + }; + + if (vm.IsPrimaryCommandCritical) + { + dialog.DefaultButton = ContentDialogButton.Close; + + // TODO: Maybe we need to style the primary button to be red? + // dialog.PrimaryButtonStyle = new Style(typeof(Button)) + // { + // Setters = + // { + // new Setter(Button.ForegroundProperty, new SolidColorBrush(Colors.Red)), + // new Setter(Button.BackgroundProperty, new SolidColorBrush(Colors.Red)), + // }, + // }; + } + + DispatcherQueue.TryEnqueue(async () => + { + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + var performMessage = new PerformCommandMessage(vm); + PerformCommand(performMessage); + } + else + { + // cancel + } + }); + } + + private void InitializeConfirmationDialog(ConfirmResultViewModel vm) + { + vm.SafeInitializePropertiesSynchronous(); + } + + private void HandleCommandResultOnUiThread(ICommandResult? result) + { + try + { + if (result != null) + { + var kind = result.Kind; + Logger.LogDebug($"handling {kind.ToString()}"); + switch (kind) + { + case CommandResultKind.Dismiss: + { + // Reset the palette to the main page and dismiss + GoHome(withAnimation: false, focusSearch: false); + WeakReferenceMessenger.Default.Send(); + break; + } + + case CommandResultKind.GoHome: + { + // Go back to the main page, but keep it open + GoHome(); + break; + } + + case CommandResultKind.GoBack: + { + GoBack(); + break; + } + + case CommandResultKind.Hide: + { + // Keep this page open, but hide the palette. + WeakReferenceMessenger.Default.Send(); + + break; + } + + case CommandResultKind.KeepOpen: + { + // Do nothing. + break; + } + + case CommandResultKind.Confirm: + { + if (result.Args is IConfirmationArgs a) + { + HandleConfirmArgs(a); + } + + break; + } + + case CommandResultKind.ShowToast: + { + if (result.Args is IToastArgs a) + { + _toast.ShowToast(a.Message); + HandleCommandResultOnUiThread(a.Result); + } + + break; + } + } + } + } + catch + { + } + } + + public void Receive(OpenSettingsMessage message) + { + _ = DispatcherQueue.TryEnqueue(() => + { + // Also hide our details pane about here, if we had one + HideDetails(); + + var settingsWindow = new SettingsWindow(); + settingsWindow.Activate(); + + WeakReferenceMessenger.Default.Send(new(null)); + }); + } + + public void Receive(ShowDetailsMessage message) + { + // TERRIBLE HACK TODO GH #245 + // There's weird wacky bugs with debounce currently. + if (!ViewModel.IsDetailsVisible) + { + ViewModel.Details = message.Details; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage))); + ViewModel.IsDetailsVisible = true; + return; + } + + // GH #322: + // For inexplicable reasons, if you try to change the details too fast, + // we'll explode. This seemingly only happens if you change the details + // while we're also scrolling a new list view item into view. + _debounceTimer.Debounce( + () => + { + ViewModel.Details = message.Details; + + // Trigger a re-evaluation of whether we have a hero image based on + // the current theme + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage))); + }, + interval: TimeSpan.FromMilliseconds(50), + immediate: ViewModel.IsDetailsVisible == false); + ViewModel.IsDetailsVisible = true; + } + + public void Receive(HideDetailsMessage message) => HideDetails(); + + public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri); + + public void Receive(HandleCommandResultMessage message) + { + DispatcherQueue.TryEnqueue(() => + { + HandleCommandResultOnUiThread(message.Result.Unsafe); + }); + } + + private void HideDetails() + { + ViewModel.Details = null; + ViewModel.IsDetailsVisible = false; + } + + public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch(); + + public void Receive(HotkeySummonMessage message) + { + _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message)); + } + + private void SummonOnUiThread(HotkeySummonMessage message) + { + var settings = App.Current.Services.GetService()!; + var commandId = message.CommandId; + var isRoot = string.IsNullOrEmpty(commandId); + if (isRoot) + { + // If this is the hotkey for the root level, then always show us + WeakReferenceMessenger.Default.Send(new(message.Hwnd)); + + // Depending on the settings, either + // * Go home, or + // * Select the search text (if we should remain open on this page) + if (settings.HotkeyGoesHome) + { + GoHome(false); + } + else if (settings.HighlightSearchOnActivate) + { + SearchBox.SelectSearch(); + } + } + else + { + try + { + // For a hotkey bound to a command, first lookup the + // command from our list of toplevel commands. + var tlcManager = App.Current.Services.GetService()!; + var topLevelCommand = tlcManager.LookupCommand(commandId); + if (topLevelCommand != null) + { + var command = topLevelCommand.Command; + var isPage = command is TopLevelCommandWrapper wrapper && wrapper.Command is not IInvokableCommand; + + // If the bound command is an invokable command, then + // we don't want to open the window at all - we want to + // just do it. + if (isPage) + { + // If we're here, then the bound command was a page + // of some kind. Let's pop the stack, show the window, and navigate to it. + GoHome(false); + + WeakReferenceMessenger.Default.Send(new(message.Hwnd)); + } + + var msg = new PerformCommandMessage(topLevelCommand) { WithAnimation = false }; + WeakReferenceMessenger.Default.Send(msg); + + // we can't necessarily SelectSearch() here, because when the page is loaded, + // we'll fetch the SearchText from the page itself, and that'll stomp the + // selection we start now. + // That's probably okay though. + } + } + catch + { + } + } + + WeakReferenceMessenger.Default.Send(); + } + + private void GoBack(bool withAnimation = true, bool focusSearch = true) + { + HideDetails(); + + // 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. + // However, then we have more fine-grained control on the back stack, managing the VM cache, and not + // having that all be a black box, though then we wouldn't cache the XAML page itself, but sometimes that is a drawback. + // However, we do a good job here, see ForwardStack.Clear below, and BackStack.Clear above about managing that. + if (withAnimation) + { + RootFrame.GoBack(); + } + else + { + RootFrame.GoBack(_noAnimation); + } + + // Don't store pages we're navigating away from in the Frame cache + // TODO: In the future we probably want a short cache (3-5?) of recent VMs in case the user re-navigates + // back to a recent page they visited (like the Pokedex) so we don't have to reload it from scratch. + // That'd be retrieved as we re-navigate in the PerformCommandMessage logic above + RootFrame.ForwardStack.Clear(); + + if (!RootFrame.CanGoBack) + { + ViewModel.GoHome(); + } + + if (focusSearch) + { + SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); + SearchBox.SelectSearch(); + } + } + + private void GoHome(bool withAnimation = true, bool focusSearch = true) + { + while (RootFrame.CanGoBack) + { + GoBack(withAnimation, focusSearch); + } + + WeakReferenceMessenger.Default.Send(); + } + + private void BackButton_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new()); + + private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e) + { + // 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 PageViewModel page) + { + // 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; + } + } + + /// + /// Gets a value indicating whether determines if the current Details have a HeroImage, given the theme + /// we're currently in. This needs to be evaluated in the view, because the + /// viewModel doesn't actually know what the current theme is. + /// + public bool HasHeroImage + { + get + { + var requestedTheme = ActualTheme; + var iconInfoVM = ViewModel.Details?.HeroImage; + return iconInfoVM?.HasIcon(requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light) ?? false; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Program.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Program.cs new file mode 100644 index 0000000000..68e6308bf6 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Program.cs @@ -0,0 +1,81 @@ +// 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 ManagedCommon; +using Microsoft.Windows.AppLifecycle; + +namespace Microsoft.CmdPal.UI; + +// cribbed heavily from +// +// https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/AppLifecycle/Instancing/cs2/cs-winui-packaged/CsWinUiDesktopInstancing +internal sealed class Program +{ + private static App? app; + + // LOAD BEARING + // + // Main cannot be async. If it is, then the clipboard won't work, and neither will narrator. + // That means you, the person thinking about making this a MTA thread. Don't + // do it. It won't work. That's not the solution. + [STAThread] + private static int Main(string[] args) + { + if (Helpers.GpoValueChecker.GetConfiguredCmdPalEnabledValue() == Helpers.GpoRuleConfiguredValue.Disabled) + { + // There's a GPO rule configured disabling CmdPal. Exit as soon as possible. + return 0; + } + + Logger.InitializeLogger("\\CmdPal\\Logs\\"); + Logger.LogDebug($"Starting at {DateTime.UtcNow}"); + + WinRT.ComWrappersSupport.InitializeComWrappers(); + bool isRedirect = DecideRedirection(); + if (!isRedirect) + { + Microsoft.UI.Xaml.Application.Start((p) => + { + Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext context = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + app = new App(); + }); + } + + return 0; + } + + private static bool DecideRedirection() + { + bool isRedirect = false; + AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs(); + AppInstance keyInstance = AppInstance.FindOrRegisterForKey("randomKey"); + + if (keyInstance.IsCurrent) + { + keyInstance.Activated += OnActivated; + } + else + { + isRedirect = true; + keyInstance.RedirectActivationToAsync(args).AsTask().ConfigureAwait(false); + } + + return isRedirect; + } + + private static void OnActivated(object? sender, AppActivationArguments args) + { + // If we already have a form, display the message now. + // Otherwise, add it to the collection for displaying later. + if (App.Current is App thisApp) + { + if (thisApp.AppWindow is not null and + MainWindow mainWindow) + { + mainWindow.Summon(string.Empty); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml new file mode 100644 index 0000000000..b95afb7240 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml @@ -0,0 +1,19 @@ + + + + + FileSystem + ARM64 + win-arm64 + win10-arm64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + False + False + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000000..5ff16b291b --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,19 @@ + + + + + FileSystem + x64 + win-x64 + win10-x64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + False + False + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json new file mode 100644 index 0000000000..68daa60f79 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Microsoft.CmdPal.UI (Package)": { + "commandName": "MsixPackage", + "nativeDebugging": false + }, + "Microsoft.CmdPal.UI (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-100.png new file mode 100644 index 0000000000..66d9712d19 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-125.png new file mode 100644 index 0000000000..4b888cbfe1 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-150.png new file mode 100644 index 0000000000..fa51fa861b Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-200.png new file mode 100644 index 0000000000..209ea5f63a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-400.png new file mode 100644 index 0000000000..2c2008979d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LargeTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LockScreenLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-100.png new file mode 100644 index 0000000000..5505cf67e9 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-125.png new file mode 100644 index 0000000000..1abda3dd69 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-150.png new file mode 100644 index 0000000000..fb8a8394bf Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-200.png new file mode 100644 index 0000000000..77716b370e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-400.png new file mode 100644 index 0000000000..325d4660cb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SmallTile.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-100.png new file mode 100644 index 0000000000..d717ecf80c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-125.png new file mode 100644 index 0000000000..76d8fd345f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-150.png new file mode 100644 index 0000000000..9b02d2f956 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..8df93bc56f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-400.png new file mode 100644 index 0000000000..5ac3f0ed66 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/SplashScreen.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..f45a4b5453 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..e4181fae93 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000..71d106afcb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..ba42b403ba Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000..b9b0b0481d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square150x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000000..effd1b294a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000000..5d19c1fd21 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000000..5ae6512bbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000000..d059c1b2fb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000000..9c47f2e758 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000000..effd1b294a Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000000..5ae6512bbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000000..d059c1b2fb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000000..9c47f2e758 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000..1afd204d26 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000..367e0540d6 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000..a50b45df0d Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..1b72c87526 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000..eb3e051dbb Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-16.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000..e70028d83f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-16.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000..96ae3ddb0c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..5d19c1fd21 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-256.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000..935a2b6dab Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-256.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-32.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000..9ae2093373 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-32.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-48.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..3c7ace9152 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Square44x44Logo.targetsize-48.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-100.png new file mode 100644 index 0000000000..5509cf4193 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-125.png new file mode 100644 index 0000000000..cc3613dc57 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-150.png new file mode 100644 index 0000000000..98e3c0f70c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-200.png new file mode 100644 index 0000000000..8e0a3c570e Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-400.png new file mode 100644 index 0000000000..2baa82f575 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/StoreLogo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-100.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000000..cac24c0150 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-100.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-125.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000000..4bf18758c5 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-125.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-150.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000000..e1c27b7366 Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-150.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..d717ecf80c Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-400.png b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000000..8df93bc56f Binary files /dev/null and b/src/modules/cmdpal/Microsoft.CmdPal.UI/Release-Assets/Wide310x150Logo.scale-400.png differ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml new file mode 100644 index 0000000000..fc4a1cb7eb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs new file mode 100644 index 0000000000..2bfdc1bcb3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml.cs @@ -0,0 +1,28 @@ +// 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.UI.ViewModels; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.CmdPal.UI.Settings; + +public sealed partial class ExtensionPage : Page +{ + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + + public ProviderSettingsViewModel? ViewModel { get; private set; } + + public ExtensionPage() + { + this.InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + ViewModel = e.Parameter is ProviderSettingsViewModel vm + ? vm + : throw new ArgumentException($"{nameof(ExtensionPage)} navigation args should be passed a {nameof(ProviderSettingsViewModel)}"); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml new file mode 100644 index 0000000000..2f3bd53f51 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs new file mode 100644 index 0000000000..e164638ebb --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs @@ -0,0 +1,38 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI.Controls; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI.Settings; + +public sealed partial class ExtensionsPage : Page +{ + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + + private readonly SettingsViewModel? viewModel; + + public ExtensionsPage() + { + this.InitializeComponent(); + + var settings = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + } + + private void SettingsCard_Click(object sender, RoutedEventArgs e) + { + if (sender is SettingsCard card) + { + if (card.DataContext is ProviderSettingsViewModel vm) + { + WeakReferenceMessenger.Default.Send(new(vm)); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml new file mode 100644 index 0000000000..c659ecff85 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs new file mode 100644 index 0000000000..d732600c4e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml.cs @@ -0,0 +1,34 @@ +// 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.UI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel; + +namespace Microsoft.CmdPal.UI.Settings; + +public sealed partial class GeneralPage : Page +{ + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + + private readonly SettingsViewModel? viewModel; + + public GeneralPage() + { + this.InitializeComponent(); + + var settings = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + } + + public string ApplicationVersion + { + get + { + var version = Package.Current.Id.Version; + return $"Version {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/NavigateToExtensionSettingsMessage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/NavigateToExtensionSettingsMessage.xaml.cs new file mode 100644 index 0000000000..6ee915a40c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/NavigateToExtensionSettingsMessage.xaml.cs @@ -0,0 +1,11 @@ +// 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.UI.ViewModels; + +namespace Microsoft.CmdPal.UI.Settings; + +public record NavigateToExtensionSettingsMessage(ProviderSettingsViewModel ProviderSettingsVM) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml new file mode 100644 index 0000000000..5b0a5bfc4c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs new file mode 100644 index 0000000000..ce6043b010 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.Pages; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Graphics; +using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; + +namespace Microsoft.CmdPal.UI.Settings; + +public sealed partial class SettingsWindow : Window, + IRecipient +{ + public ObservableCollection BreadCrumbs { get; } = []; + + public SettingsWindow() + { + this.InitializeComponent(); + this.ExtendsContentIntoTitleBar = true; + this.AppWindow.SetIcon("ms-appx:///Assets/Icons/StoreLogo.png"); + this.AppWindow.Title = RS_.GetString("SettingsWindowTitle"); + this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall; + PositionCentered(); + WeakReferenceMessenger.Default.Register(this); + } + + private void NavView_Loaded(object sender, RoutedEventArgs e) + { + NavView.SelectedItem = NavView.MenuItems[0]; + Navigate("General"); + } + + private void NavView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + var selectedItem = args.InvokedItem; + + if (selectedItem is not null) + { + Navigate(selectedItem.ToString()!); + } + } + + private void Navigate(string page) + { + var pageType = page switch + { + "General" => typeof(GeneralPage), + "Extensions" => typeof(ExtensionsPage), + _ => null, + }; + if (pageType is not null) + { + BreadCrumbs.Clear(); + BreadCrumbs.Add(new(page, page)); + NavFrame.Navigate(pageType); + } + } + + private void Navigate(ProviderSettingsViewModel extension) + { + NavFrame.Navigate(typeof(ExtensionPage), extension); + BreadCrumbs.Add(new(extension.DisplayName, string.Empty)); + } + + private void PositionCentered() + { + AppWindow.Resize(new SizeInt32 { Width = 1280, Height = 720 }); + var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest); + if (displayArea is not null) + { + var centeredPosition = AppWindow.Position; + centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2; + centeredPosition.Y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2; + AppWindow.Move(centeredPosition); + } + } + + public void Receive(NavigateToExtensionSettingsMessage message) => Navigate(message.ProviderSettingsVM); + + private void NavigationBreadcrumbBar_ItemClicked(BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs args) + { + if (args.Item is Crumb crumb) + { + if (crumb.Data is string data) + { + if (!string.IsNullOrEmpty(data)) + { + Navigate(data); + } + } + } + } + + private void Window_Activated(object sender, WindowActivatedEventArgs args) + { + WeakReferenceMessenger.Default.Send(args); + } +} + +public readonly struct Crumb +{ + public Crumb(string label, object data) + { + Label = label; + Data = data; + } + + public string Label { get; } + + public object Data { get; } + + public override string ToString() => Label; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..bee46656f5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Command Palette + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette Dev + {Locked} The dev build will never be seen in multiple languages + + + Command Palette Canary + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette Preview + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Windows Command Palette + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Windows Command Palette Dev + {Locked} The dev build will never be seen in multiple languages + + + Windows Command Palette Canary + {Locked=qps-ploc,qps-ploca,qps-plocm}. "Canary" in this context means an unstable or nightly build of a software product, not the bird. + + + Windows Command Palette Preview + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette Dev + {Locked} The dev build will never be seen in multiple languages + + + Command Palette Canary + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette Preview + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + The Windows Command Palette + + + A dev build of the Command Palette + {Locked} The dev build will never be seen in multiple languages + + + The Windows Command Palette (Canary build) + {Locked} + + + The Windows Command Palette (Preview build) + + + Command Palette + {Locked=qps-ploc,qps-ploca,qps-plocm} + + + Command Palette Dev + {Locked} The dev build will never be seen in multiple languages + + + Cancel + + + Press a combination of keys to change this shortcut + + + Press a combination of keys to change this shortcut. +Right-click to remove the key combination, thereby deactivating the shortcut. + + + Reset + + + Save + + + Activation shortcut + + + Invalid shortcut + + + Only shortcuts that start with **Windows key**, **Ctrl**, **Alt** or **Shift** are valid. + The ** sequences are used for text formatting of the key names. Don't remove them on translation. + + + Possible shortcut interference with Alt Gr + Alt Gr refers to the right alt key on some international keyboards + + + Shortcuts with **Ctrl** and **Alt** may remove functionality from some international keyboards, because **Ctrl** + **Alt** = **Alt Gr** in those keyboards. + The ** sequences are used for text formatting of the key names. Don't remove them on translation. + + + Command Palette settings + A section header for app-wide settings. "Command Palette" is the name of the app. + + + About + A section header for information about the app + + + About + A section header for information about the app + + + Extension settings + A section header for extension-specific settings. + + + Commands + A section header for information about the app + + + Command Palette Settings + The title of the settings window for the app + + + Command Palette Toast + The title of the toast window for the command palette + + + Type here to search... + + + Preferred monitor position + as in Show Command Palette on primary monitor + + + If multiple monitors are in use, Command Palette can be launched on the desired monitor + as in Show Command Palette on primary monitor + + + Monitor with mouse cursor + + + Monitor with focused window + + + Primary monitor + + + Don't move + + + Confirm + + + Cancel + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml new file mode 100644 index 0000000000..3c091b7ff3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml new file mode 100644 index 0000000000..880e9f4eb0 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Settings.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Settings.xaml new file mode 100644 index 0000000000..f145be5483 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Settings.xaml @@ -0,0 +1,25 @@ + + + + + + + + 4 + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TextBox.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TextBox.xaml new file mode 100644 index 0000000000..26e61dda4e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TextBox.xaml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + +