From 8f9534121f4e76168bd9514df8c7cc279b7801c6 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 24 Jan 2025 06:49:18 -0600 Subject: [PATCH] Initial draft of the winget extension (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _targets #355_, which I need for improvements to messages * [x] The initial package load takes a long time. This is pretty much unavoidable, but we do it on cmdpal startup, so anything after about 12s should be snappy * [x] I cannot for the life of me get `FindPackagesAsync`, to be async. The call always ends up running synchronously, so I can't hook up the `operation.Completed` event, nor the cancellation. The action is already complete! - this is probably blocking, because we still end up doing a search on most keystrokes, so we only get the final results after all the intermediate ones are done. - Just pasting a search though? Just as snappy as you'd hope. - Ahahahaha it wasn't me: [microsoft/winget-cli#5151](https://github.com/microsoft/winget-cli/pull/5151) - ✅ manually wrapping this in a BG thread made it better * [ ] We probably shouldn't make the default action for an installed package "Uninstall". - Probably want to shunt over to the Settings app for the package - We probably want to do the thing where the second command doesn't show up if it's a separator - Punt? punt * [x] We need to add more metadata in the details for packages. We have it, if only we could show it: #95 - This will be a follow-up * [ ] This needs localization too * I'm using the `1.10-preview` of the winget com interfaces. On my framework laptop at least, the `RefreshPackageCatalogAsync` API isn't yet implemented, so I need to test that * [x] I don't think we implemented `MoreCommands` being observable in the host yet. We should. - Punted, #360 * [ ] I probably also need to check if other APIs we're using exist or not * [x] I haven't tested situations that like, need you to accept a license? Installing `nano` and the NanoLeaf app both _just work_. - Punted? --- .github/actions/spell-check/expect.txt | 3 + Directory.Packages.props | 1 + PowerToys.sln | 111 +++++--- .../CommandPaletteHost.cs | 7 + .../CommandProviderWrapper.cs | 2 +- .../DetailsDataViewModel.cs | 12 + .../DetailsElementViewModel.cs | 27 ++ .../DetailsLinkViewModel.cs | 41 +++ .../DetailsSeparatorViewModel.cs | 21 ++ .../DetailsTagsViewModel.cs | 28 ++ .../DetailsViewModel.cs | 26 +- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 10 +- .../DetailsDataTemplateSelector.xaml.cs | 37 +++ .../ExtViews/ListPage.xaml | 2 +- .../Microsoft.CmdPal.UI.csproj | 1 + .../Microsoft.CmdPal.UI/SettingsPage.xaml | 8 +- .../cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml | 100 +++++-- .../Microsoft.CmdPal.UI/ShellPage.xaml.cs | 2 + .../CommandProvider.cs | 2 +- .../DetailsLink.cs | 2 +- .../Assets/AppList.scale-100.png | Bin 0 -> 1441 bytes .../ExtensionHostInstance.cs | 81 ++++++ .../Microsoft.CmdPal.Ext.WinGet.csproj | 72 +++++ .../NativeMethods.txt | 6 + .../Pages/InstallPackageCommand.cs | 208 ++++++++++++++ .../Pages/InstallPackageListItem.cs | 156 +++++++++++ .../Pages/InstalledPackagesPage.cs | 84 ++++++ .../Pages/WinGetExtensionPage.cs | 257 ++++++++++++++++++ .../WinGetExtensionCommandsProvider.cs | 39 +++ .../WinGetExtensionHost.cs | 10 + .../WinGetStatics.cs | 104 +++++++ .../ClassModel.cs | 47 ++++ .../ClassesDefinition.cs | 120 ++++++++ .../ClsidContext.cs | 14 + .../WindowsPackageManagerFactory.cs | 59 ++++ .../WindowsPackageManagerStandardFactory.cs | 40 +++ .../Microsoft.CmdPal.Ext.WinGet/app.manifest | 19 ++ 37 files changed, 1669 insertions(+), 90 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsDataViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsElementViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsLinkViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsSeparatorViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsTagsViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/DetailsDataTemplateSelector.xaml.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Assets/AppList.scale-100.png create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/ExtensionHostInstance.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Microsoft.CmdPal.Ext.WinGet.csproj create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/NativeMethods.txt create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstalledPackagesPage.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionHost.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassModel.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassesDefinition.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClsidContext.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerFactory.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerStandardFactory.cs create mode 100644 src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/app.manifest diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 1fd5d1c36efa..257013ccfeec 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -204,6 +204,7 @@ CLIPSIBLINGS closesocket CLSCTX CLSIDs +Clsids Clusion cmder CMDNOTFOUNDMODULEINTERFACE @@ -972,6 +973,8 @@ msctls msdata MSDL MSGFLT +MSHCTX +MSHLFLAGS MSIDXS MSIDXSPROP msiexec diff --git a/Directory.Packages.props b/Directory.Packages.props index 7fe8f2df399a..6b2888838edd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,6 +47,7 @@ + diff --git a/PowerToys.sln b/PowerToys.sln index cae308528419..73c3e0681e69 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -706,6 +706,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "src\modules\AdvancedPaste\AdvancedPaste.FuzzTests\AdvancedPaste.FuzzTests.csproj", "{7F5B9557-5878-4438-A721-3E28296BA193}" 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ZoomIt", "ZoomIt", "{DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomIt", "src\modules\ZoomIt\ZoomIt\ZoomIt.vcxproj", "{0A84F764-3A88-44CD-AA96-41BDBD48627B}" @@ -3236,6 +3238,18 @@ Global {8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.ActiveCfg = Release|x64 {8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.Build.0 = Release|x64 {8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.Deploy.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 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.Build.0 = Debug|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.ActiveCfg = Debug|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.Build.0 = Debug|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.ActiveCfg = Release|ARM64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.Build.0 = Release|ARM64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.ActiveCfg = Release|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.Build.0 = Release|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.ActiveCfg = Release|x64 + {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.Build.0 = Release|x64 {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Debug|ARM64.Build.0 = Debug|ARM64 {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Debug|ARM64.Deploy.0 = Debug|ARM64 @@ -3254,30 +3268,6 @@ Global {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.ActiveCfg = Release|x64 {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.Build.0 = Release|x64 {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.Deploy.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 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.Build.0 = Debug|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.ActiveCfg = Debug|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.Build.0 = Debug|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.ActiveCfg = Release|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.Build.0 = Release|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.ActiveCfg = Release|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.Build.0 = Release|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.ActiveCfg = Release|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.Build.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}.Debug|x86.ActiveCfg = Debug|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x86.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 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x86.ActiveCfg = Release|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x86.Build.0 = Release|x64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|ARM64.Build.0 = Debug|ARM64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|x64.ActiveCfg = Debug|x64 @@ -3290,6 +3280,36 @@ Global {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.Build.0 = Release|x64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x86.ActiveCfg = Release|x64 {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.Build.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.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 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.ActiveCfg = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x86.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 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x86.ActiveCfg = Release|x64 + {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x86.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 @@ -3326,24 +3346,6 @@ Global {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x64.Build.0 = Release|x64 {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x86.ActiveCfg = Release|x64 {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.Build.0 = Debug|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.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 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.ActiveCfg = Release|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.Build.0 = Release|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.Deploy.0 = Release|x64 {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.ActiveCfg = Debug|ARM64 {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.Build.0 = Debug|ARM64 {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x64.ActiveCfg = Debug|x64 @@ -3356,6 +3358,24 @@ Global {7F5B9557-5878-4438-A721-3E28296BA193}.Release|x64.Build.0 = Release|x64 {7F5B9557-5878-4438-A721-3E28296BA193}.Release|x86.ActiveCfg = Release|x64 {7F5B9557-5878-4438-A721-3E28296BA193}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x86.Build.0 = Debug|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x86.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 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x86.ActiveCfg = Release|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x86.Build.0 = Release|x64 + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x86.Deploy.0 = Release|x64 {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|ARM64.Build.0 = Debug|ARM64 {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|x64.ActiveCfg = Debug|x64 @@ -3652,16 +3672,17 @@ Global {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8} = {3846508C-77EB-4034-A702-F8BB263C4F79} {D8DD2E06-7956-4673-95E7-F395AB5A5485} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} {8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} - {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} {89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {C0CE3B5E-16D3-495D-B335-CA791B660162} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79} {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} {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE} = {9873BA05-4C41-4819-9283-CF45D795431B} {7F5B9557-5878-4438-A721-3E28296BA193} = {9873BA05-4C41-4819-9283-CF45D795431B} + {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} {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} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs index ce86d7ced7d6..42bcc6c3d1df 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs @@ -28,6 +28,8 @@ public sealed partial class CommandPaletteHost : IExtensionHost public IExtensionWrapper? Extension { get; } + private readonly ICommandProvider? _builtInProvider; + private CommandPaletteHost() { } @@ -37,6 +39,11 @@ public CommandPaletteHost(IExtensionWrapper source) Extension = source; } + public CommandPaletteHost(ICommandProvider builtInProvider) + { + _builtInProvider = builtInProvider; + } + public IAsyncAction ShowStatus(IStatusMessage? message) { if (message == null) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 7e02c09c9bbe..bd88cc9834fb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -45,7 +45,7 @@ public CommandProviderWrapper(ICommandProvider provider) _commandProvider = new(provider); // Hook the extension back into us - ExtensionHost = CommandPaletteHost.Instance; + ExtensionHost = new CommandPaletteHost(provider); _commandProvider.Unsafe!.InitializeWithHost(ExtensionHost); _commandProvider.Unsafe!.ItemsChanged += CommandProvider_ItemsChanged; 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 000000000000..5a0b2a95f37e --- /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.Extensions; +using Microsoft.CmdPal.UI.ViewModels.Models; + +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 000000000000..f3983d661cd5 --- /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.Extensions; +using Microsoft.CmdPal.UI.ViewModels.Models; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public abstract partial class DetailsElementViewModel(IDetailsElement _detailsElement, IPageContext 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 000000000000..7d51e8215303 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsLinkViewModel.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.Extensions; +using Microsoft.CmdPal.UI.ViewModels.Models; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsLinkViewModel( + IDetailsElement _detailsElement, + IPageContext 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; + 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 000000000000..4b5fc23ece22 --- /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.Extensions; +using Microsoft.CmdPal.UI.ViewModels.Models; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsSeparatorViewModel( + IDetailsElement _detailsElement, + IPageContext 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 000000000000..d7558a7ba067 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsTagsViewModel.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.Extensions; +using Microsoft.CmdPal.UI.ViewModels.Models; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class DetailsTagsViewModel( + IDetailsElement _detailsElement, + IPageContext context) : DetailsElementViewModel(_detailsElement, context) +{ + private readonly ExtensionObject _dataModel = + new(_detailsElement.Data as IDetailsTags); + + public override void InitializeProperties() + { + base.InitializeProperties(); + var model = _dataModel.Unsafe; + if (model == null) + { + return; + } + + // TODO! + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs index 266aff2f4c5f..d8af34c827c5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs @@ -15,12 +15,14 @@ public partial class DetailsViewModel(IDetails _details, IPageContext context) : // cannot be marked [ObservableProperty] public IconInfoViewModel HeroImage { get; private set; } = new(null); - // TODO: Metadata is an array of IDetailsElement, - // where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator} 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; @@ -37,5 +39,25 @@ public override void 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/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 49cff2e3625c..6917b7b06cb4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -14,6 +14,7 @@ using Microsoft.CmdPal.Ext.WindowsSettings; using Microsoft.CmdPal.Ext.WindowsTerminal; using Microsoft.CmdPal.Ext.WindowWalker; +using Microsoft.CmdPal.Ext.WinGet; using Microsoft.CmdPal.Extensions; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; @@ -82,12 +83,13 @@ private static ServiceProvider ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Models services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/DetailsDataTemplateSelector.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/DetailsDataTemplateSelector.xaml.cs new file mode 100644 index 000000000000..328e0ca01e5d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/DetailsDataTemplateSelector.xaml.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/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index 0b31628e5151..b0306417b33e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -45,7 +45,7 @@ - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index 154f1a8d1616..cad8113cc23e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -106,6 +106,7 @@ + True diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/SettingsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/SettingsPage.xaml index dc19edcfbee7..0da202bf853f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/SettingsPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/SettingsPage.xaml @@ -72,14 +72,12 @@ HeaderIcon="{ui:FontIcon Glyph=}"> - + - + - + @@ -26,11 +27,41 @@ + + + + + + + + + + + + + + + + + @@ -155,48 +186,58 @@ IsNavigationStackEnabled="True" Navigated="RootFrame_Navigated" /> - + - - - - - + + + + + + - + - + - - - + + + + - - + IsIndeterminate="{x:Bind ViewModel.CurrentPage.MostRecentStatusMessage.Progress.IsIndeterminate, Mode=OneWay}" + Visibility="{x:Bind ViewModel.CurrentPage.MostRecentStatusMessage.HasProgress, Mode=OneWay}" + Value="{x:Bind ViewModel.CurrentPage.MostRecentStatusMessage.Progress.ProgressValue, Mode=OneWay}" /> + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs index af32e75fef82..055360cac282 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml.cs @@ -12,6 +12,8 @@ using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Animation; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs index e8f6f694c3c2..32581a7ecaa9 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/CommandProvider.cs @@ -40,7 +40,7 @@ public abstract partial class CommandProvider : ICommandProvider public bool Frozen { get; protected set; } = true; - public void InitializeWithHost(IExtensionHost host) + public virtual void InitializeWithHost(IExtensionHost host) { ExtensionHost.Initialize(host); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/DetailsLink.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/DetailsLink.cs index f6daa16de765..b1ad775743e7 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/DetailsLink.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/DetailsLink.cs @@ -6,7 +6,7 @@ namespace Microsoft.CmdPal.Extensions.Helpers; public class DetailsLink : IDetailsLink { - public Uri Link { get; set; } = new(string.Empty); + public Uri? Link { get; set; } public string Text { get; set; } = string.Empty; } diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Assets/AppList.scale-100.png b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Assets/AppList.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..843a4dad95f82a280376def12959980067c8a45c GIT binary patch literal 1441 zcmV;S1z!4zP)dB zTvZhQ-n`ijyPN%yG{TCRV53E}@DGWx679rNA-atyf(SNN79&`wRVq;tL`zA)E=3}{ z8&^Aj5wuaq6%xTB8a`{w?*(EHPsNcmD2a?Yh-jwRS^?mIpG2B1BjKX?1O%@mL&?sxO-$%T~_# z{oLytL^#cxtZ7yx*%1>!_MZhIec+iU75bcWQnf6|_+3kZX?K`}_I;;59;xL@hDpe_ z0_zqvf6@Ri0MSpkxf!zKNYrAQi@7+fy@afEC$A#DGkWRY~DiZ;1J& zBc3q@{8n#(yfJ4|8M*h$%oc-hdcew4#6d?s_{%NZh3X=jsaXzt?c3+Xao>W_bdf++E(KvZ9A+H_6X}sFSY-W*3A2b zqqG6Ea_mvu@!%;=VK;E;yEC}*;}@aGSinLMmRl!q=ffKigDx(8`x*9r-A+g-%Bs$4 zvVRrlhwk-%I5!V5mp841fK+;< z>r9nZ7Y=5aq9UPzx}f4ugRhj|pZ_3eZ{#N~4yNy?%}@E=xOl|H$DCeW;i#!}!+?UL}2v zUtXBQ%|E|1pn*!J)qDl^<{w*FS;~1+8JKlffTYy#mtmbl$=@l+vaPhHo;e@w>SB{Y zU&L1o2o5w+!n7LeGT7CnrUfgs<9=SuwgtZrz+usF;Ss(Nz&@3h73l8nMeLk;+b|kw zSoL*xH+fFZr()$L(hHB5Tm-zz#^+%f-Cs)=A5*CsoQ75wa_sGHGn<5a9(xW8i_7@t z+E&i4aJBc}Nju6Y|04a%z%o vqCymkgv;H+IzW2@!#6Rrnaylw`?7xl9Y8KYseAYM00000NkvXXu0mjfCBw1` literal 0 HcmV?d00001 diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/ExtensionHostInstance.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/ExtensionHostInstance.cs new file mode 100644 index 000000000000..2c436efa3441 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/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.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WinGet; + +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) + { + if (Host != null) + { + _ = Task.Run(async () => + { + try + { + await Host.ShowStatus(message); + } + 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/exts/Microsoft.CmdPal.Ext.WinGet/Microsoft.CmdPal.Ext.WinGet.csproj b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Microsoft.CmdPal.Ext.WinGet.csproj new file mode 100644 index 000000000000..a791f4fc4232 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Microsoft.CmdPal.Ext.WinGet.csproj @@ -0,0 +1,72 @@ + + + + Microsoft.CmdPal.Ext.WinGet + app.manifest + win-$(Platform).pubxml + false + enable + true + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + Microsoft.Management.Deployment + $(OutDir) + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + NU1701 + true + none + + + + + + + + + + + true + + diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/NativeMethods.txt b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/NativeMethods.txt new file mode 100644 index 000000000000..8ce59af095f7 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/NativeMethods.txt @@ -0,0 +1,6 @@ +CoCreateInstance +CoMarshalInterface +CoUnmarshalInterface +CreateStreamOnHGlobal +MSHCTX +MSHLFLAGS \ No newline at end of file diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs new file mode 100644 index 000000000000..9a82286f1b0f --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation +// 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 System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Management.Deployment; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.WinGet.Pages; + +public partial class InstallPackageCommand : InvokableCommand +{ + private readonly CatalogPackage _package; + + private readonly StatusMessage _installBanner = new(); + private IAsyncOperationWithProgress? _installAction; + private IAsyncOperationWithProgress? _unInstallAction; + private Task? _installTask; + + public bool IsInstalled { get; private set; } + + public static IconInfo CompletedIcon { get; } = new("\uE930"); // Completed + + public static IconInfo DownloadIcon { get; } = new("\uE896"); // Download + + public event EventHandler? InstallStateChanged; + + public InstallPackageCommand(CatalogPackage package, bool isInstalled) + { + _package = package; + IsInstalled = isInstalled; + UpdateAppearance(); + } + + internal void FakeChangeStatus() + { + IsInstalled = !IsInstalled; + UpdateAppearance(); + } + + private void UpdateAppearance() + { + Icon = IsInstalled ? CompletedIcon : DownloadIcon; + Name = IsInstalled ? "Uninstall" : "Install"; + } + + public override ICommandResult Invoke() + { + // TODO: LOCK in here, so this can only be invoked once until the + // install / uninstall is done. Just use like, an atomic + if (_installTask != null) + { + return CommandResult.KeepOpen(); + } + + if (IsInstalled) + { + // Uninstall + _installBanner.State = MessageState.Info; + _installBanner.Message = $"Uninstalling {_package.Name}..."; + WinGetExtensionHost.Instance.ShowStatus(_installBanner); + + var installOptions = WinGetStatics.WinGetFactory.CreateUninstallOptions(); + installOptions.PackageUninstallScope = PackageUninstallScope.Any; + _unInstallAction = WinGetStatics.Manager.UninstallPackageAsync(_package, installOptions); + + var handler = new AsyncOperationProgressHandler(OnUninstallProgress); + _unInstallAction.Progress = handler; + + _installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction)); + } + else + { + // Install + _installBanner.State = MessageState.Info; + _installBanner.Message = $"Installing {_package.Name}..."; + WinGetExtensionHost.Instance.ShowStatus(_installBanner); + + var installOptions = WinGetStatics.WinGetFactory.CreateInstallOptions(); + installOptions.PackageInstallScope = PackageInstallScope.Any; + _installAction = WinGetStatics.Manager.InstallPackageAsync(_package, installOptions); + + var handler = new AsyncOperationProgressHandler(OnInstallProgress); + _installAction.Progress = handler; + + _installTask = Task.Run(() => TryDoInstallOperation(_installAction)); + } + + return CommandResult.KeepOpen(); + } + + private async void TryDoInstallOperation( + IAsyncOperationWithProgress action) + { + try + { + await action.AsTask(); + _installBanner.Message = $"Finished {(IsInstalled ? "uninstall" : "install")} for {_package.Name}"; + _installBanner.Progress = null; + _installBanner.State = MessageState.Success; + _installTask = null; + + _ = Task.Run(() => + { + Thread.Sleep(2500); + if (_installTask == null) + { + WinGetExtensionHost.Instance.HideStatus(_installBanner); + } + }); + InstallStateChanged?.Invoke(this, this); + } + catch (Exception ex) + { + _installBanner.State = MessageState.Error; + _installBanner.Progress = null; + _installBanner.Message = ex.Message; + _installTask = null; + } + } + + private static string FormatBytes(ulong bytes) + { + const long KB = 1024; + const long MB = KB * 1024; + const long GB = MB * 1024; + + return bytes >= GB + ? $"{bytes / (double)GB:F2} GB" + : bytes >= MB ? + $"{bytes / (double)MB:F2} MB" + : bytes >= KB + ? $"{bytes / (double)KB:F2} KB" + : $"{bytes} bytes"; + } + + private void OnInstallProgress( + IAsyncOperationWithProgress operation, + InstallProgress progress) + { + var downloadText = "Downloading. "; + switch (progress.State) + { + case PackageInstallProgressState.Queued: + _installBanner.Message = $"Queued {_package.Name} for download..."; + break; + case PackageInstallProgressState.Downloading: + if (progress.BytesRequired > 0) + { + downloadText += $"{FormatBytes(progress.BytesDownloaded)} of {FormatBytes(progress.BytesRequired)}"; + _installBanner.Progress ??= new ProgressState() { IsIndeterminate = false }; + ((ProgressState)_installBanner.Progress).ProgressPercent = (uint)(progress.BytesDownloaded / progress.BytesRequired * 100); + _installBanner.Message = downloadText; + } + + break; + case PackageInstallProgressState.Installing: + _installBanner.Message = $"Installing {_package.Name}..."; + _installBanner.Progress = new ProgressState() { IsIndeterminate = true }; + break; + case PackageInstallProgressState.PostInstall: + _installBanner.Message = $"Finishing install for {_package.Name}..."; + break; + case PackageInstallProgressState.Finished: + _installBanner.Message = "Finished install."; + + // progressBar.IsIndeterminate(false); + _installBanner.Progress = null; + _installBanner.State = MessageState.Success; + break; + default: + _installBanner.Message = string.Empty; + break; + } + } + + private void OnUninstallProgress( + IAsyncOperationWithProgress operation, + UninstallProgress progress) + { + switch (progress.State) + { + case PackageUninstallProgressState.Queued: + _installBanner.Message = $"Queued {_package.Name} for uninstall..."; + break; + + case PackageUninstallProgressState.Uninstalling: + _installBanner.Message = $"Uninstalling {_package.Name}..."; + _installBanner.Progress = new ProgressState() { IsIndeterminate = true }; + break; + case PackageUninstallProgressState.PostUninstall: + _installBanner.Message = $"Finishing uninstall for {_package.Name}..."; + break; + case PackageUninstallProgressState.Finished: + _installBanner.Message = "Finished uninstall."; + _installBanner.Progress = null; + _installBanner.State = MessageState.Success; + break; + default: + _installBanner.Message = string.Empty; + break; + } + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs new file mode 100644 index 000000000000..900e9e770b12 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation +// 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.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Management.Deployment; +using Windows.Foundation.Metadata; + +namespace Microsoft.CmdPal.Ext.WinGet.Pages; + +public partial class InstallPackageListItem : ListItem +{ + private readonly CatalogPackage _package; + private InstallPackageCommand? _installCommand; + + public InstallPackageListItem(CatalogPackage package) + : base(new NoOpCommand()) + { + _package = package; + + var version = _package.DefaultInstallVersion; + var versionText = version.Version; + var versionTagText = versionText == "Unknown" && version.PackageCatalog.Info.Id == "StoreEdgeFD" ? "msstore" : versionText; + + Title = _package.Name; + Subtitle = _package.Id; + Tags = [new Tag() { Text = versionTagText }]; + + var metadata = version.GetCatalogPackageMetadata(); + if (metadata != null) + { + var detailsBody = $""" +## {metadata.Publisher} + +{metadata.Description} +"""; + IconInfo heroIcon = new(string.Empty); + var icons = metadata.Icons; + if (icons.Count > 0) + { + // There's also a .Theme property we could probably use to + // switch between default or individual icons. + heroIcon = new IconInfo(icons[0].Url); + } + + Details = new Details() + { + Body = detailsBody, + Title = metadata.PackageName, + HeroImage = heroIcon, + Metadata = GetDetailsMetadata(metadata).ToArray(), + }; + } + + _ = Task.Run(UpdatedInstalledStatus); + } + + private List GetDetailsMetadata(CatalogPackageMetadata metadata) + { + List detailsElements = new(); + + // key -> {text, url} + Dictionary simpleData = new() + { + { "Author", (metadata.Author, string.Empty) }, + { "Copyright", (metadata.Copyright, metadata.CopyrightUrl) }, + { "License", (metadata.License, metadata.LicenseUrl) }, + { "Publisher", (metadata.Publisher, metadata.PublisherUrl) }, + { "Publisher Support", (string.Empty, metadata.PublisherSupportUrl) }, + }; + var docs = metadata.Documentations.ToArray(); + foreach (var item in docs) + { + simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl)); + } + + foreach (var kv in simpleData) + { + var text = string.IsNullOrEmpty(kv.Value.Item1) ? kv.Value.Item2 : kv.Value.Item1; + var target = kv.Value.Item2; + if (!string.IsNullOrEmpty(text)) + { + Uri? uri = null; + try + { + uri = new Uri(target); + } + catch (System.UriFormatException) + { + } + + var pair = new DetailsElement() + { + Key = kv.Key, + Data = new DetailsLink() { Link = uri, Text = text }, + }; + detailsElements.Add(pair); + } + } + + return detailsElements; + } + + private async void UpdatedInstalledStatus() + { + var status = await _package.CheckInstalledStatusAsync(); + var isInstalled = _package.InstalledVersion != null; + _installCommand = new InstallPackageCommand(_package, isInstalled); + this.Command = _installCommand; + Icon = _installCommand.Icon; + + _installCommand.InstallStateChanged += InstallStateChangedHandler; + } + + private void InstallStateChangedHandler(object? sender, InstallPackageCommand e) + { + if (!ApiInformation.IsApiContractPresent("Microsoft.Management.Deployment", 12)) + { + Debug.WriteLine($"RefreshPackageCatalogAsync isn't available"); + e.FakeChangeStatus(); + Command = e; + Icon = Command.Icon; + return; + } + + _ = Task.Run(() => + { + Stopwatch s = new(); + Debug.WriteLine($"Starting RefreshPackageCatalogAsync"); + s.Start(); + var refs = WinGetStatics.AvailableCatalogs.ToArray(); + + foreach (var catalog in refs) + { + var operation = catalog.RefreshPackageCatalogAsync(); + operation.Wait(); + } + + s.Stop(); + Debug.WriteLine($" RefreshPackageCatalogAsync took {s.ElapsedMilliseconds}ms"); + }).ContinueWith((previous) => + { + if (previous.IsCompletedSuccessfully) + { + Debug.WriteLine($"Updating InstalledStatus"); + UpdatedInstalledStatus(); + } + }); + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstalledPackagesPage.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstalledPackagesPage.cs new file mode 100644 index 000000000000..8a61c02d028c --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/InstalledPackagesPage.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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Management.Deployment; + +namespace Microsoft.CmdPal.Ext.WinGet.Pages; + +public partial class InstalledPackagesPage : ListPage +{ + public InstalledPackagesPage() + { + Icon = new("\uE74C"); + Name = "Installed Packages"; + IsLoading = true; + } + + internal async Task GetLocalCatalog() + { + var catalogRef = WinGetStatics.Manager.GetLocalPackageCatalog(LocalPackageCatalog.InstalledPackages); + var connectResult = await catalogRef.ConnectAsync(); + var compositeCatalog = connectResult.PackageCatalog; + return compositeCatalog; + } + + public override IListItem[] GetItems() + { + var fetchAsync = FetchLocalPackagesAsync(); + fetchAsync.ConfigureAwait(false); + var results = fetchAsync.Result; + IListItem[] listItems = !results.Any() + ? [ + new ListItem(new NoOpCommand()) + { + Title = "No packages found", + } + ] + : results.Select(p => + { + var versionText = p.InstalledVersion?.Version ?? string.Empty; + + Tag[] tags = string.IsNullOrEmpty(versionText) ? [] : [new Tag() { Text = versionText }]; + return new ListItem(new NoOpCommand()) + { + Title = p.Name, + Subtitle = p.Id, + Tags = tags, + }; + }).ToArray(); + IsLoading = false; + return listItems; + } + + private async Task> FetchLocalPackagesAsync() + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + + var results = new HashSet(new PackageIdCompare()); + + var catalog = await GetLocalCatalog(); + var opts = WinGetStatics.WinGetFactory.CreateFindPackagesOptions(); + var searchResults = await catalog.FindPackagesAsync(opts); + foreach (var match in searchResults.Matches.ToArray()) + { + // Print the packages + var package = match.CatalogPackage; + results.Add(package); + } + + stopwatch.Stop(); + + Debug.WriteLine($"Search took {stopwatch.ElapsedMilliseconds}"); + + return results; + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs new file mode 100644 index 000000000000..9d113f02df7e --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation +// 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.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WinGet.Pages; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Management.Deployment; + +namespace Microsoft.CmdPal.Ext.WinGet; + +internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable +{ + private readonly string _tag = string.Empty; + + public bool HasTag => !string.IsNullOrEmpty(_tag); + + private readonly Lock _resultsLock = new(); + + private CancellationTokenSource? _cancellationTokenSource; + private Task>? _currentSearchTask; + + private IEnumerable? _results; + + public static IconInfo WinGetIcon { get; } = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Assets\\AppList.scale-100.png")); + + private readonly StatusMessage _errorMessage = new() { State = MessageState.Error }; + + public WinGetExtensionPage(string tag = "") + { + Icon = WinGetIcon; + Name = "Search Winget"; + _tag = tag; + ShowDetails = true; + } + + public override IListItem[] GetItems() + { + IListItem[] items = []; + lock (_resultsLock) + { + var emptySearchForTag = _results == null && + string.IsNullOrEmpty(SearchText) && + HasTag; + + if (emptySearchForTag) + { + IsLoading = true; + DoUpdateSearchText(string.Empty); + return items; + } + + items = (_results == null || !_results.Any()) + ? [ + new ListItem(new NoOpCommand()) + { + Title = (string.IsNullOrEmpty(SearchText) && !HasTag) ? + "Start typing to search for packages" : + "No packages found", + } + ] + : _results.Select(PackageToListItem).ToArray(); + } + + IsLoading = false; + + return items; + } + + private static ListItem PackageToListItem(CatalogPackage p) => new InstallPackageListItem(p); + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (newSearch == oldSearch) + { + return; + } + + DoUpdateSearchText(newSearch); + } + + private void DoUpdateSearchText(string newSearch) + { + // Cancel any ongoing search + if (_cancellationTokenSource != null) + { + Debug.WriteLine("Cancelling old search"); + _cancellationTokenSource.Cancel(); + } + + _cancellationTokenSource = new CancellationTokenSource(); + + var cancellationToken = _cancellationTokenSource.Token; + + IsLoading = true; + + // Save the latest search task + _currentSearchTask = DoSearchAsync(newSearch, cancellationToken); + + // Await the task to ensure only the latest one gets processed + _ = ProcessSearchResultsAsync(_currentSearchTask, newSearch); + } + + private async Task ProcessSearchResultsAsync( + Task> searchTask, + string newSearch) + { + try + { + var results = await searchTask; + + // Ensure this is still the latest task + if (_currentSearchTask == searchTask) + { + // Process the results (e.g., update UI) + UpdateWithResults(results, newSearch); + } + } + catch (OperationCanceledException) + { + // Handle cancellation gracefully (e.g., log or ignore) + Debug.WriteLine($" Cancelled search for '{newSearch}'"); + } + catch (Exception ex) + { + // Handle other exceptions + Console.WriteLine(ex.Message); + } + } + + private void UpdateWithResults(IEnumerable results, string query) + { + Debug.WriteLine($"Completed search for '{query}'"); + lock (_resultsLock) + { + this._results = results; + } + + RaiseItemsChanged(this._results.Count()); + } + + private async Task> DoSearchAsync(string query, CancellationToken ct) + { + // Were we already canceled? + ct.ThrowIfCancellationRequested(); + + Stopwatch stopwatch = new(); + stopwatch.Start(); + + if (string.IsNullOrEmpty(query) + && string.IsNullOrEmpty(_tag)) + { + return []; + } + + var searchDebugText = $"{query}{(HasTag ? "+" : string.Empty)}{_tag}"; + Debug.WriteLine($"Starting search for '{searchDebugText}'"); + var results = new HashSet(new PackageIdCompare()); + + // Default selector: this is the way to do a `winget search ` + var selector = WinGetStatics.WinGetFactory.CreatePackageMatchFilter(); + selector.Field = Microsoft.Management.Deployment.PackageMatchField.CatalogDefault; + selector.Value = query; + selector.Option = PackageFieldMatchOption.ContainsCaseInsensitive; + + var opts = WinGetStatics.WinGetFactory.CreateFindPackagesOptions(); + opts.Selectors.Add(selector); + + // testing + opts.ResultLimit = 25; + + // Selectors is "OR", Filters is "AND" + if (HasTag) + { + var tagFilter = WinGetStatics.WinGetFactory.CreatePackageMatchFilter(); + tagFilter.Field = Microsoft.Management.Deployment.PackageMatchField.Tag; + tagFilter.Value = query; + tagFilter.Option = PackageFieldMatchOption.ContainsCaseInsensitive; + + opts.Filters.Add(tagFilter); + } + + // Clean up here, then... + ct.ThrowIfCancellationRequested(); + + var catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog; + + // Both these catalogs should have been instantiated by the + // WinGetStatics static ctor when we were created. + var catalog = await catalogTask.Value; + + if (catalog == null) + { + // This error should have already been displayed by WinGetStatics + return []; + } + + // foreach (var catalog in connections) + { + Debug.WriteLine($" Searching {catalog.Info.Name} ({query})"); + + ct.ThrowIfCancellationRequested(); + + // BODGY, re: microsoft/winget-cli#5151 + // FindPackagesAsync isn't actually async. + var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct); + var searchResults = await internalSearchTask; + + // TOOD more error handling like this: + if (searchResults.Status != FindPackagesResultStatus.Ok) + { + _errorMessage.Message = $"Unexpected error: {searchResults.Status}"; + WinGetExtensionHost.Instance.ShowStatus(_errorMessage); + return []; + } + + Debug.WriteLine($" got results for ({query})"); + foreach (var match in searchResults.Matches.ToArray()) + { + ct.ThrowIfCancellationRequested(); + + // Print the packages + var package = match.CatalogPackage; + + results.Add(package); + } + + Debug.WriteLine($" ({searchDebugText}): count: {results.Count}"); + } + + stopwatch.Stop(); + + Debug.WriteLine($"Search \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms"); + + return results; + } + + public void Dispose() => throw new NotImplementedException(); +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "I just like it")] +public sealed class PackageIdCompare : IEqualityComparer +{ + public bool Equals(CatalogPackage? x, CatalogPackage? y) => + (x?.Id == y?.Id) + && (x?.DefaultInstallVersion?.PackageCatalog == y?.DefaultInstallVersion?.PackageCatalog); + + public int GetHashCode([DisallowNull] CatalogPackage obj) => obj.Id.GetHashCode(); +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs new file mode 100644 index 000000000000..a03e1bb126a6 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.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 Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WinGet; + +public partial class WinGetExtensionCommandsProvider : CommandProvider +{ + public WinGetExtensionCommandsProvider() + { + DisplayName = "WinGet"; + Id = "WinGet"; + Icon = WinGetExtensionPage.WinGetIcon; + + _ = WinGetStatics.Manager; + } + + private readonly ICommandItem[] _commands = [ + new ListItem(new WinGetExtensionPage()), + + // new ListItem( + // new Microsoft.CmdPal.Ext.WinGetPage("command-line") { Title = "tag:command-line" }) + // { + // Title = "Search for command-line packages", + // }, + + // new ListItem(new InstalledPackagesPage()) + ]; + + public override ICommandItem[] TopLevelCommands() => _commands; + + public override void InitializeWithHost(IExtensionHost host) + { + WinGetExtensionHost.Instance.Initialize(host); + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionHost.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionHost.cs new file mode 100644 index 000000000000..20995b51f52c --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionHost.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.Ext.WinGet; + +public partial class WinGetExtensionHost +{ + internal static ExtensionHostInstance Instance { get; } = new(); +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs new file mode 100644 index 000000000000..59c13adbd042 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Management.Deployment; +using WindowsPackageManager.Interop; + +namespace Microsoft.CmdPal.Ext.WinGet; + +internal static class WinGetStatics +{ + public static WindowsPackageManagerStandardFactory WinGetFactory { get; private set; } + + public static PackageManager Manager { get; private set; } + + public static IReadOnlyList AvailableCatalogs { get; private set; } + + private static readonly PackageCatalogReference _wingetCatalog; + private static readonly PackageCatalogReference _storeCatalog; + + public static Lazy> CompositeAllCatalog { get; } = new(() => GetCompositeCatalog(true)); + + public static Lazy> CompositeWingetCatalog { get; } = new(() => GetCompositeCatalog(false)); + + private static readonly StatusMessage _errorMessage = new() { State = MessageState.Error }; + + static WinGetStatics() + { + WinGetFactory = new WindowsPackageManagerStandardFactory(); + + // Create Package Manager and get available catalogs + Manager = WinGetFactory.CreatePackageManager(); + + _wingetCatalog = Manager.GetPredefinedPackageCatalog(PredefinedPackageCatalog.OpenWindowsCatalog); + _storeCatalog = Manager.GetPredefinedPackageCatalog(PredefinedPackageCatalog.MicrosoftStore); + AvailableCatalogs = [ + _wingetCatalog, + _storeCatalog, + ]; + + foreach (var catalogReference in AvailableCatalogs) + { + catalogReference.PackageCatalogBackgroundUpdateInterval = new(0); + } + + // Immediately start the lazy-init of the all packages catalog, but + // leave the winget one to be initialized as needed + _ = Task.Run(() => + { + _ = CompositeAllCatalog.Value; + + // _ = CompositeWingetCatalog.Value; + }); + } + + internal static async Task GetCompositeCatalog(bool all) + { + Stopwatch stopwatch = new(); + Debug.WriteLine($"Starting GetCompositeCatalog({all}) fetch"); + stopwatch.Start(); + + // Create the composite catalog + var createCompositePackageCatalogOptions = WinGetFactory.CreateCreateCompositePackageCatalogOptions(); + + if (all) + { + // Add winget and the store to this catalog + foreach (var catalogReference in AvailableCatalogs.ToArray()) + { + createCompositePackageCatalogOptions.Catalogs.Add(catalogReference); + } + } + else + { + createCompositePackageCatalogOptions.Catalogs.Add(_wingetCatalog); + } + + // Searches only the catalogs provided, but will correlated with installed items + createCompositePackageCatalogOptions.CompositeSearchBehavior = CompositeSearchBehavior.RemotePackagesFromAllCatalogs; + + var catalogRef = WinGetStatics.Manager.CreateCompositePackageCatalog(createCompositePackageCatalogOptions); + + var connectResult = await catalogRef.ConnectAsync(); + var compositeCatalog = connectResult.PackageCatalog; + + stopwatch.Stop(); + Debug.WriteLine($"GetCompositeCatalog({all}) fetch took {stopwatch.ElapsedMilliseconds}ms"); + + if (connectResult.Status == ConnectResultStatus.CatalogError) + { + _errorMessage.Message = $"Error {connectResult.ExtendedErrorCode.HResult}. Are you connected to the internet?"; + WinGetExtensionHost.Instance.ShowStatus(_errorMessage); + } + + return compositeCatalog; + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassModel.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassModel.cs new file mode 100644 index 000000000000..53258fa1044c --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassModel.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.Collections.Generic; +using WindowsPackageManager.Interop; + +namespace Microsoft.CmdPal.Ext.WinGet.WindowsPackageManager.Interop; + +#nullable disable +internal sealed class ClassModel +{ + /// + /// Gets the interface for the projected class type generated by CsWinRT + /// + public Type InterfaceType { get; init; } + + /// + /// Gets the projected class type generated by CsWinRT + /// + public Type ProjectedClassType { get; init; } + + /// + /// Gets the Clsids for each context (e.g. OutOfProcProd, OutOfProcDev) + /// + public IReadOnlyDictionary Clsids { get; init; } + + /// + /// Get CLSID based on the provided context + /// + /// Context + /// CLSID for the provided context. + /// Throw an exception if the clsid context is not available for the current instance. + public Guid GetClsid(ClsidContext context) + { + return !Clsids.TryGetValue(context, out var clsid) + ? throw new InvalidOperationException($"{ProjectedClassType.FullName} is not implemented in context {context}") + : clsid; + } + + /// + /// Get IID corresponding to the COM object + /// + /// IID. + public Guid GetIid() => InterfaceType.GUID; +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassesDefinition.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassesDefinition.cs new file mode 100644 index 000000000000..9108537799f8 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClassesDefinition.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 Microsoft.CmdPal.Ext.WinGet.WindowsPackageManager.Interop; +using Microsoft.Management.Deployment; + +namespace WindowsPackageManager.Interop; + +internal static class ClassesDefinition +{ + private static Dictionary Classes { get; } = new() + { + [typeof(PackageManager)] = new() + { + ProjectedClassType = typeof(PackageManager), + InterfaceType = typeof(IPackageManager), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("C53A4F16-787E-42A4-B304-29EFFB4BF597"), + [ClsidContext.Dev] = new Guid("74CB3139-B7C5-4B9E-9388-E6616DEA288C"), + }, + }, + + [typeof(FindPackagesOptions)] = new() + { + ProjectedClassType = typeof(FindPackagesOptions), + InterfaceType = typeof(IFindPackagesOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("572DED96-9C60-4526-8F92-EE7D91D38C1A"), + [ClsidContext.Dev] = new Guid("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"), + }, + }, + + [typeof(CreateCompositePackageCatalogOptions)] = new() + { + ProjectedClassType = typeof(CreateCompositePackageCatalogOptions), + InterfaceType = typeof(ICreateCompositePackageCatalogOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("526534B8-7E46-47C8-8416-B1685C327D37"), + [ClsidContext.Dev] = new Guid("EE160901-B317-4EA7-9CC6-5355C6D7D8A7"), + }, + }, + + [typeof(InstallOptions)] = new() + { + ProjectedClassType = typeof(InstallOptions), + InterfaceType = typeof(IInstallOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("1095F097-EB96-453B-B4E6-1613637F3B14"), + [ClsidContext.Dev] = new Guid("44FE0580-62F7-44D4-9E91-AA9614AB3E86"), + }, + }, + + [typeof(UninstallOptions)] = new() + { + ProjectedClassType = typeof(UninstallOptions), + InterfaceType = typeof(IUninstallOptions), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"), + [ClsidContext.Dev] = new Guid("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"), + }, + }, + + [typeof(PackageMatchFilter)] = new() + { + ProjectedClassType = typeof(PackageMatchFilter), + InterfaceType = typeof(IPackageMatchFilter), + Clsids = new Dictionary() + { + [ClsidContext.Prod] = new Guid("D02C9DAF-99DC-429C-B503-4E504E4AB000"), + [ClsidContext.Dev] = new Guid("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"), + }, + }, + }; + + /// + /// Get CLSID based on the provided context for the specified type + /// + /// Projected class type + /// Context + /// CLSID for the provided context and type, or throw an exception if not found. + /// Throws an exception if type is not a project class. + public static Guid GetClsid(ClsidContext context) + { + ValidateType(); + return Classes[typeof(T)].GetClsid(context); + } + + /// + /// Get IID corresponding to the COM object + /// + /// Projected class type + /// IID or throw an exception if not found. + /// Throws an exception if type is not a project class. + public static Guid GetIid() + { + ValidateType(); + return Classes[typeof(T)].GetIid(); + } + + /// + /// Validate that the provided type is defined. + /// + /// Projected class type + /// Throws an exception if type is not a project class. + private static void ValidateType() + { + if (!Classes.ContainsKey(typeof(TType))) + { + throw new InvalidOperationException($"{typeof(TType).Name} is not a projected class type."); + } + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClsidContext.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClsidContext.cs new file mode 100644 index 000000000000..09629a903c78 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/ClsidContext.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 WindowsPackageManager.Interop; + +public enum ClsidContext +{ + // Production CLSID Guids + Prod, + + // Development CLSID Guids + Dev, +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerFactory.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerFactory.cs new file mode 100644 index 000000000000..cd9a3cab66e9 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerFactory.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; +using Microsoft.Management.Deployment; + +namespace WindowsPackageManager.Interop; + +/// +/// Factory class for creating WinGet COM objects. +/// Details about each method can be found in the source IDL: +/// https://github.com/microsoft/winget-cli/blob/master/src/Microsoft.Management.Deployment/PackageManager.idl +/// +public abstract class WindowsPackageManagerFactory +{ + private readonly ClsidContext _clsidContext; + + public WindowsPackageManagerFactory(ClsidContext clsidContext) + { + _clsidContext = clsidContext; + } + + /// + /// Creates an instance of the class . + /// + /// + /// Type must be one of the types defined in the winget COM API. + /// Implementations of this method can assume that and + /// are the right GUIDs for the class in the given context. + /// + protected abstract T CreateInstance(Guid clsid, Guid iid); + + public PackageManager CreatePackageManager() => CreateInstance(); + + public FindPackagesOptions CreateFindPackagesOptions() => CreateInstance(); + + public CreateCompositePackageCatalogOptions CreateCreateCompositePackageCatalogOptions() => CreateInstance(); + + public InstallOptions CreateInstallOptions() => CreateInstance(); + + public UninstallOptions CreateUninstallOptions() => CreateInstance(); + + public PackageMatchFilter CreatePackageMatchFilter() => CreateInstance(); + + /// + /// Creates an instance of the class . + /// + /// + /// This is a helper for calling the derived class's + /// method with the appropriate GUIDs. + /// + private T CreateInstance() + { + var clsid = ClassesDefinition.GetClsid(_clsidContext); + var iid = ClassesDefinition.GetIid(); + return CreateInstance(clsid, iid); + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerStandardFactory.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerStandardFactory.cs new file mode 100644 index 000000000000..d8b6064a86ff --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/WindowsPackageManager.Interop/WindowsPackageManagerStandardFactory.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.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.System.Com; +using WinRT; + +namespace WindowsPackageManager.Interop; + +public class WindowsPackageManagerStandardFactory : WindowsPackageManagerFactory +{ + public WindowsPackageManagerStandardFactory(ClsidContext clsidContext = ClsidContext.Prod) + : base(clsidContext) + { + } + + protected override T CreateInstance(Guid clsid, Guid iid) + { + var pUnknown = IntPtr.Zero; + try + { + var hr = PInvoke.CoCreateInstance(clsid, null, CLSCTX.CLSCTX_ALL, iid, out var result); + Marshal.ThrowExceptionForHR(hr); + pUnknown = Marshal.GetIUnknownForObject(result); + return MarshalGeneric.FromAbi(pUnknown); + } + finally + { + // CoCreateInstance and FromAbi both AddRef on the native object. + // Release once to prevent memory leak. + if (pUnknown != IntPtr.Zero) + { + Marshal.Release(pUnknown); + } + } + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/app.manifest b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/app.manifest new file mode 100644 index 000000000000..bcafb9bc5b8d --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + +