From c589cfa9e8793368f96ec7dc171240e1f7ee18a3 Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Fri, 5 Dec 2025 10:21:47 -0600 Subject: [PATCH] Continuing part deux --- Directory.Packages.props | 2 +- PowerToys.sln | 49 +- .../AnonymousCommand.cs | 28 + .../BaseObservable.cs | 31 + .../ChoiceSetSetting.cs | 75 +++ .../ClipboardHelper.cs | 298 ++++++++++ .../ColorHelpers.cs | 16 + .../Command.cs | 36 ++ .../CommandContextItem.cs | 41 ++ .../CommandItem.cs | 135 +++++ .../CommandProvider.cs | 72 +++ .../CommandResult.cs | 92 +++ .../Commands/ConfirmableCommand.cs | 155 +++++ .../Commands/CopyPathCommand.cs | 47 ++ .../Commands/CopyTextCommand.cs | 25 + .../Commands/NoOpCommand.cs | 10 + .../Commands/OpenFileCommand.cs | 45 ++ .../Commands/OpenInConsoleCommand.cs | 48 ++ .../Commands/OpenPropertiesCommand.cs | 63 ++ .../Commands/OpenUrlCommand.cs | 25 + .../Commands/OpenWithCommand.cs | 57 ++ .../Commands/ShowFileInFolderCommand.cs | 39 ++ .../ConfirmationArgs.cs | 16 + .../ContentPage.cs | 38 ++ .../Details.cs | 56 ++ .../DetailsCommands.cs | 10 + .../DetailsElement.cs | 12 + .../DetailsLink.cs | 31 + .../DetailsSeparator.cs | 9 + .../DetailsTags.cs | 10 + .../DynamicListPage.cs | 21 + .../ExtensionHost.cs | 76 +++ .../ExtensionInstanceManager`1.cs | 129 ++++ .../ExtensionServer.cs | 101 ++++ .../FallbackCommandItem.cs | 35 ++ .../Filter.cs | 44 ++ .../Filters.cs | 23 + .../FontIconData.cs | 32 + .../FormContent.cs | 48 ++ .../FuzzyStringMatcher.cs | 182 ++++++ .../GalleryGridLayout.cs | 32 + .../GlobalSuppressions.cs | 8 + .../GoToPageArgs.cs | 12 + .../ISettingsForm.cs | 20 + .../IconData.cs | 30 + .../IconHelpers.cs | 15 + .../IconInfo.cs | 46 ++ .../InvokableCommand.cs | 12 + .../ItemsChangedEventArgs.cs | 17 + .../JsonSerializationContext.cs | 22 + .../JsonSettingsManager.cs | 100 ++++ .../KeyChordHelpers.cs | 61 ++ .../ListHelpers.cs | 175 ++++++ .../ListItem.cs | 69 +++ .../ListPage.cs | 113 ++++ .../LogMessage.cs | 37 ++ .../ManagedCsWin32/Shell32.cs | 36 ++ .../MarkdownContent.cs | 29 + .../MatchOption.cs | 25 + .../MatchResult.cs | 69 +++ .../MediumGridLayout.cs | 20 + ...t.CommandPalette.Extensions.Toolkit.csproj | 78 +++ .../NativeMethods.cs | 102 ++++ .../NativeMethods.txt | 11 + .../Page.cs | 42 ++ .../ProgressState.cs | 32 + .../PropChangedEventArgs.cs | 17 + .../Properties/Resources.Designer.cs | 180 ++++++ .../Properties/Resources.resx | 160 +++++ .../SearchPrecisionScore.cs | 12 + .../Separator.cs | 9 + .../Setting`1.cs | 80 +++ .../Settings.cs | 131 ++++ .../SettingsForm.cs | 38 ++ .../ShellHelpers.cs | 281 +++++++++ .../SmallGridLayout.cs | 9 + .../StatusMessage.cs | 42 ++ .../Tag.cs | 75 +++ .../TextSetting.cs | 60 ++ .../ThumbnailHelper.cs | 435 ++++++++++++++ .../ToastArgs.cs | 12 + .../ToastStatusMessage.cs | 47 ++ .../ToggleSetting.cs | 114 ++++ .../TreeContent.cs | 38 ++ .../Utilities.cs | 83 +++ .../WeakEventListener`3.cs | 80 +++ .../Microsoft.CommandPalette.Extensions.def | 3 + .../Microsoft.CommandPalette.Extensions.idl | 396 +++++++++++++ ...icrosoft.CommandPalette.Extensions.vcxproj | 183 ++++++ .../packages.config | 17 + .../pch.cpp | 1 + .../Microsoft.CommandPalette.Extensions/pch.h | 4 + .../readme.txt | 2 + .../til/winrt.h | 112 ++++ .../winrt_module.cpp | 58 ++ .../AppStateModel.cs | 58 ++ .../Helpers/LayoutMapHelper.cs | 22 + .../HotkeySettings.cs | 231 ++++++++ .../JsonSerializationContext.cs | 23 + .../Messages/HotkeySummonMessage.cs | 9 + .../Messages/OpenSettingsMessage.cs | 9 + .../Messages/QuitMessage.cs | 12 + .../Microsoft.CommandPalette.UI.Models.csproj | 46 ++ .../MonitorBehavior.cs | 14 + .../Services/PersistenceService.cs | 164 +++++ .../SettingsModel.cs | 148 +++++ .../WindowPosition.cs | 53 ++ ...rosoft.CommandPalette.UI.ViewModels.csproj | 29 + .../SettingsViewModel.cs | 189 ++++++ .../ShellViewModel.cs | 47 ++ .../UI/Microsoft.CommandPalette.UI/App.xaml | 7 +- .../Microsoft.CommandPalette.UI/App.xaml.cs | 29 +- .../MainWindow.xaml | 28 +- .../MainWindow.xaml.cs | 11 +- .../Microsoft.CommandPalette.UI.csproj | 11 + .../Package-Dev.appxmanifest | 12 +- .../Pages/ShellPage.xaml | 203 +++++++ .../Pages/ShellPage.xaml.cs | 19 + .../Services/TrayIconService.cs | 212 +++++++ .../Strings/en-us/Resources.resw | 559 ++++++++++++++++++ .../Styles/Colors.xaml | 40 ++ .../Styles/Settings.xaml | 25 + .../Styles/TextBlock.xaml | 27 + .../Styles/TextBox.xaml | 281 +++++++++ 124 files changed, 8640 insertions(+), 32 deletions(-) create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/AnonymousCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/BaseObservable.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ChoiceSetSetting.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ClipboardHelper.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ColorHelpers.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Command.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandContextItem.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandResult.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ConfirmableCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyPathCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyTextCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/NoOpCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenFileCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenInConsoleCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenPropertiesCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenUrlCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenWithCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ShowFileInFolderCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ConfirmationArgs.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ContentPage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Details.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsCommands.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsElement.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsLink.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsSeparator.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsTags.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DynamicListPage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionHost.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionInstanceManager`1.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionServer.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filter.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FormContent.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GalleryGridLayout.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GlobalSuppressions.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GoToPageArgs.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ISettingsForm.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconHelpers.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/InvokableCommand.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ItemsChangedEventArgs.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSettingsManager.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/KeyChordHelpers.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListPage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/LogMessage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ManagedCsWin32/Shell32.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MarkdownContent.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchOption.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchResult.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MediumGridLayout.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.txt create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Page.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ProgressState.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/PropChangedEventArgs.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.resx create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SearchPrecisionScore.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Setting`1.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SmallGridLayout.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/StatusMessage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Tag.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastArgs.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastStatusMessage.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TreeContent.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Utilities.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/WeakEventListener`3.cs create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.def create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/packages.config create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.cpp create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.h create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/readme.txt create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/til/winrt.h create mode 100644 src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/winrt_module.cpp create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/LayoutMapHelper.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/HotkeySettings.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/HotkeySummonMessage.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/OpenSettingsMessage.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/QuitMessage.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Microsoft.CommandPalette.UI.Models.csproj create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/MonitorBehavior.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/WindowPosition.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/ShellViewModel.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Services/TrayIconService.cs create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Strings/en-us/Resources.resw create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Colors.xaml create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Settings.xaml create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBlock.xaml create mode 100644 src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBox.xaml diff --git a/Directory.Packages.props b/Directory.Packages.props index 6744b991aa..bc6e328f6b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,7 +69,7 @@ This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail. --> - + diff --git a/PowerToys.sln b/PowerToys.sln index 4d1a39dc58..1667745fea 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11222.15 d18.0 +VisualStudioVersion = 18.0.11222.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner.vcxproj", "{9412D5C6-2CF2-4FC2-A601-B55508EA9B27}" ProjectSection(ProjectDependencies) = postProject @@ -844,6 +844,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{4F881A97-423A- EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CommandPalette.UI", "src\modules\Deux\UI\Microsoft.CommandPalette.UI\Microsoft.CommandPalette.UI.csproj", "{9EBE6DE4-58CD-CA14-7A21-B1E9DED261DB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CommandPalette.UI.ViewModels", "src\modules\Deux\UI\Microsoft.CommandPalette.UI.ViewModels\Microsoft.CommandPalette.UI.ViewModels.csproj", "{9D1D4795-6E0A-44E7-9C91-61CA07421F2E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{ED6F2337-189A-4E98-A481-316347D319BF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.CommandPalette.Extensions", "src\modules\Deux\SDK\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.vcxproj", "{7997DAD4-31D6-496B-95DB-6C028D699370}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CommandPalette.Extensions.Toolkit", "src\modules\Deux\SDK\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj", "{7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CommandPalette.UI.Models", "src\modules\Deux\UI\Microsoft.CommandPalette.UI.Models\Microsoft.CommandPalette.UI.Models.csproj", "{D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -3074,6 +3084,38 @@ Global {9EBE6DE4-58CD-CA14-7A21-B1E9DED261DB}.Release|x64.ActiveCfg = Release|x64 {9EBE6DE4-58CD-CA14-7A21-B1E9DED261DB}.Release|x64.Build.0 = Release|x64 {9EBE6DE4-58CD-CA14-7A21-B1E9DED261DB}.Release|x64.Deploy.0 = Release|x64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Debug|ARM64.Build.0 = Debug|ARM64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Debug|x64.ActiveCfg = Debug|x64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Debug|x64.Build.0 = Debug|x64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Release|ARM64.ActiveCfg = Release|ARM64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Release|ARM64.Build.0 = Release|ARM64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Release|x64.ActiveCfg = Release|x64 + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E}.Release|x64.Build.0 = Release|x64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Debug|ARM64.Build.0 = Debug|ARM64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Debug|x64.ActiveCfg = Debug|x64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Debug|x64.Build.0 = Debug|x64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Release|ARM64.ActiveCfg = Release|ARM64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Release|ARM64.Build.0 = Release|ARM64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Release|x64.ActiveCfg = Release|x64 + {7997DAD4-31D6-496B-95DB-6C028D699370}.Release|x64.Build.0 = Release|x64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Debug|ARM64.Build.0 = Debug|ARM64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Debug|x64.ActiveCfg = Debug|x64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Debug|x64.Build.0 = Debug|x64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Release|ARM64.ActiveCfg = Release|ARM64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Release|ARM64.Build.0 = Release|ARM64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Release|x64.ActiveCfg = Release|x64 + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675}.Release|x64.Build.0 = Release|x64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Debug|ARM64.Build.0 = Debug|ARM64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Debug|x64.ActiveCfg = Debug|x64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Debug|x64.Build.0 = Debug|x64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Release|ARM64.ActiveCfg = Release|ARM64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Release|ARM64.Build.0 = Release|ARM64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Release|x64.ActiveCfg = Release|x64 + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3410,6 +3452,11 @@ Global {023C058E-537A-4AB6-900A-36437EC17410} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {4F881A97-423A-4905-B219-677AD0184059} = {023C058E-537A-4AB6-900A-36437EC17410} {9EBE6DE4-58CD-CA14-7A21-B1E9DED261DB} = {4F881A97-423A-4905-B219-677AD0184059} + {9D1D4795-6E0A-44E7-9C91-61CA07421F2E} = {4F881A97-423A-4905-B219-677AD0184059} + {ED6F2337-189A-4E98-A481-316347D319BF} = {023C058E-537A-4AB6-900A-36437EC17410} + {7997DAD4-31D6-496B-95DB-6C028D699370} = {ED6F2337-189A-4E98-A481-316347D319BF} + {7FA183E0-ADB2-8F18-0E6B-A724BB0D1675} = {ED6F2337-189A-4E98-A481-316347D319BF} + {D985116D-16D6-9BE7-0371-9E7EAA2FF2B5} = {4F881A97-423A-4905-B219-677AD0184059} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/AnonymousCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/AnonymousCommand.cs new file mode 100644 index 0000000000..4c6450706f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/AnonymousCommand.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed partial class AnonymousCommand : InvokableCommand +{ + private readonly Action? _action; + + public ICommandResult Result { get; set; } = CommandResult.Dismiss(); + + public AnonymousCommand(Action? action) + { + Name = Properties.Resources.AnonymousCommand_Invoke; + _action = action; + } + + public override ICommandResult Invoke() + { + if (_action is not null) + { + _action(); + } + + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/BaseObservable.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/BaseObservable.cs new file mode 100644 index 0000000000..92303397dc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/BaseObservable.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +// TODO! We probably want to have OnPropertyChanged raise the event +// asynchronously, so as to not block the extension app while it's being +// processed in the host app. +// (also consider this for ItemsChanged in ListPage) +public partial class BaseObservable : INotifyPropChanged +{ + public event TypedEventHandler? PropChanged; + + protected void OnPropertyChanged(string propertyName) + { + try + { + // TODO #181 - This is dangerous! If the original host goes away, + // this can crash as we try to invoke the handlers from that process. + // However, just catching it seems to still raise the event on the + // new host? + PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName)); + } + catch + { + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ChoiceSetSetting.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ChoiceSetSetting.cs new file mode 100644 index 0000000000..51beb0b5e7 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ChoiceSetSetting.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.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed class ChoiceSetSetting : Setting +{ + public partial class Choice + { + [JsonPropertyName("value")] + public string Value { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + public Choice(string title, string value) + { + Value = value; + Title = title; + } + } + + public List Choices { get; set; } + + private ChoiceSetSetting() + : base() + { + Choices = []; + } + + public ChoiceSetSetting(string key, List choices) + : base(key, choices.ElementAt(0).Value) + { + Choices = choices; + } + + public ChoiceSetSetting(string key, string label, string description, List choices) + : base(key, label, description, choices.ElementAt(0).Value) + { + Choices = choices; + } + + public override Dictionary ToDictionary() + { + return new Dictionary + { + { "type", "Input.ChoiceSet" }, + { "title", Label }, + { "id", Key }, + { "label", Description }, + { "choices", Choices }, + { "value", Value ?? string.Empty }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + }; + } + + public static ChoiceSetSetting LoadFromJson(JsonObject jsonObject) => new() { Value = jsonObject["value"]?.GetValue() ?? string.Empty }; + + public override void Update(JsonObject payload) + { + // If the key doesn't exist in the payload, don't do anything + if (payload[Key] is not null) + { + Value = payload[Key]?.GetValue(); + } + } + + public override string ToState() => $"\"{Key}\": {JsonSerializer.Serialize(Value, JsonSerializationContext.Default.String)}"; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ClipboardHelper.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ClipboardHelper.cs new file mode 100644 index 0000000000..b2a8e65bc6 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ClipboardHelper.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation +// The 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.CommandPalette.Extensions.Toolkit; + +// shamelessly from https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Management/commands/management/Clipboard.cs +public static partial class ClipboardHelper +{ + private static readonly bool? _clipboardSupported = true; + + // Used if an external clipboard is not available, e.g. if xclip is missing. + // This is useful for testing in CI as well. + private static string? _internalClipboard; + + public static string GetText() + { + if (_clipboardSupported == false) + { + return _internalClipboard ?? string.Empty; + } + + var tool = string.Empty; + var args = string.Empty; + var clipboardText = string.Empty; + + ExecuteOnStaThread(() => GetTextImpl(out clipboardText)); + return clipboardText; + } + + public static void SetText(string text) + { + if (_clipboardSupported == false) + { + _internalClipboard = text; + return; + } + + var tool = string.Empty; + var args = string.Empty; + ExecuteOnStaThread(() => SetClipboardData(Tuple.Create(text, CF_UNICODETEXT))); + return; + } + + public static void SetRtf(string plainText, string rtfText) + { + if (s_CF_RTF == 0) + { + s_CF_RTF = RegisterClipboardFormat("Rich Text Format"); + } + + ExecuteOnStaThread(() => SetClipboardData( + Tuple.Create(plainText, CF_UNICODETEXT), + Tuple.Create(rtfText, s_CF_RTF))); + } + +#pragma warning disable SA1310 // Field names should not contain underscore + private const uint GMEM_MOVEABLE = 0x0002; + private const uint GMEM_ZEROINIT = 0x0040; +#pragma warning restore SA1310 // Field names should not contain underscore + private const uint GHND = GMEM_MOVEABLE | GMEM_ZEROINIT; + + [LibraryImport("kernel32.dll")] + private static partial IntPtr GlobalAlloc(uint flags, UIntPtr dwBytes); + + [LibraryImport("kernel32.dll")] + private static partial IntPtr GlobalFree(IntPtr hMem); + + [LibraryImport("kernel32.dll")] + private static partial IntPtr GlobalLock(IntPtr hMem); + + [LibraryImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GlobalUnlock(IntPtr hMem); + + [LibraryImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] + private static partial void CopyMemory(IntPtr dest, IntPtr src, uint count); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool IsClipboardFormatAvailable(uint format); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool OpenClipboard(IntPtr hWndNewOwner); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool CloseClipboard(); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool EmptyClipboard(); + + [LibraryImport("user32.dll")] + private static partial IntPtr GetClipboardData(uint format); + + [LibraryImport("user32.dll")] + private static partial IntPtr SetClipboardData(uint format, IntPtr data); + + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + private static partial uint RegisterClipboardFormat(string lpszFormat); + +#pragma warning disable SA1310 // Field names should not contain underscore + private const uint CF_TEXT = 1; + private const uint CF_UNICODETEXT = 13; + +#pragma warning disable SA1308 // Variable names should not be prefixed + private static uint s_CF_RTF; +#pragma warning restore SA1308 // Variable names should not be prefixed +#pragma warning restore SA1310 // Field names should not contain underscore + + private static bool GetTextImpl(out string text) + { + try + { + if (IsClipboardFormatAvailable(CF_UNICODETEXT)) + { + if (OpenClipboard(IntPtr.Zero)) + { + var data = GetClipboardData(CF_UNICODETEXT); + if (data != IntPtr.Zero) + { + data = GlobalLock(data); + text = Marshal.PtrToStringUni(data) ?? string.Empty; + GlobalUnlock(data); + return true; + } + } + } + else if (IsClipboardFormatAvailable(CF_TEXT)) + { + if (OpenClipboard(IntPtr.Zero)) + { + var data = GetClipboardData(CF_TEXT); + if (data != IntPtr.Zero) + { + data = GlobalLock(data); + text = Marshal.PtrToStringAnsi(data) ?? string.Empty; + GlobalUnlock(data); + return true; + } + } + } + } + catch + { + // Ignore exceptions + } + finally + { + CloseClipboard(); + } + + text = string.Empty; + return false; + } + + private static bool SetClipboardData(params Tuple[] data) + { + try + { + if (!OpenClipboard(IntPtr.Zero)) + { + return false; + } + + EmptyClipboard(); + + foreach (var d in data) + { + if (!SetSingleClipboardData(d.Item1, d.Item2)) + { + return false; + } + } + } + finally + { + CloseClipboard(); + } + + return true; + } + + private static bool SetSingleClipboardData(string text, uint format) + { + var hGlobal = IntPtr.Zero; + var data = IntPtr.Zero; + + try + { + uint bytes; + if (format == s_CF_RTF || format == CF_TEXT) + { + bytes = (uint)(text.Length + 1); + data = Marshal.StringToHGlobalAnsi(text); + } + else if (format == CF_UNICODETEXT) + { + bytes = (uint)((text.Length + 1) * 2); + data = Marshal.StringToHGlobalUni(text); + } + else + { + // Not yet supported format. + return false; + } + + if (data == IntPtr.Zero) + { + return false; + } + + hGlobal = GlobalAlloc(GHND, (UIntPtr)bytes); + if (hGlobal == IntPtr.Zero) + { + return false; + } + + var dataCopy = GlobalLock(hGlobal); + if (dataCopy == IntPtr.Zero) + { + return false; + } + + CopyMemory(dataCopy, data, bytes); + GlobalUnlock(hGlobal); + + if (SetClipboardData(format, hGlobal) != IntPtr.Zero) + { + // The clipboard owns this memory now, so don't free it. + hGlobal = IntPtr.Zero; + } + } + catch + { + // Ignore failures + } + finally + { + if (data != IntPtr.Zero) + { + Marshal.FreeHGlobal(data); + } + + if (hGlobal != IntPtr.Zero) + { + GlobalFree(hGlobal); + } + } + + return true; + } + + private static void ExecuteOnStaThread(Func action) + { + const int RetryCount = 5; + var tries = 0; + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + while (tries++ < RetryCount && !action()) + { + // wait until RetryCount or action + } + + return; + } + + Exception? exception = null; + var thread = new Thread(() => + { + try + { + while (tries++ < RetryCount && !action()) + { + // wait until RetryCount or action + } + } + catch (Exception e) + { + exception = e; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + if (exception is not null) + { + throw exception; + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ColorHelpers.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ColorHelpers.cs new file mode 100644 index 0000000000..19b35b0ba0 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ColorHelpers.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed class ColorHelpers +{ + public static OptionalColor FromArgb(byte a, byte r, byte g, byte b) => new(true, new(r, g, b, a)); + + public static OptionalColor FromRgb(byte r, byte g, byte b) => new(true, new(r, g, b, 255)); + + public static OptionalColor Transparent() => new(true, new(0, 0, 0, 0)); + + public static OptionalColor NoColor() => new(false, new(0, 0, 0, 0)); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Command.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Command.cs new file mode 100644 index 0000000000..d7e8c4c796 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Command.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class Command : BaseObservable, ICommand +{ + public virtual string Name + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Name)); + } + } + += string.Empty; + + public virtual string Id { get; set; } = string.Empty; + + public virtual IconInfo Icon + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Icon)); + } + } + += new(); + + IIconInfo ICommand.Icon => Icon; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandContextItem.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandContextItem.cs new file mode 100644 index 0000000000..467953381d --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandContextItem.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class CommandContextItem : CommandItem, ICommandContextItem +{ + public virtual bool IsCritical { get; set; } + + public virtual KeyChord RequestedShortcut { get; set; } + + public CommandContextItem(ICommand command) + : base(command) + { + } + + public CommandContextItem( + string title, + string subtitle = "", + string name = "", + Action? action = null, + ICommandResult? result = null) + { + var c = new AnonymousCommand(action); + if (!string.IsNullOrEmpty(name)) + { + c.Name = name; + } + + if (result is not null) + { + c.Result = result; + } + + Command = c; + + Title = title; + Subtitle = subtitle; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs new file mode 100644 index 0000000000..cddb678fa3 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft 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.CommandPalette.Extensions.Toolkit; + +public partial class CommandItem : BaseObservable, ICommandItem +{ + private ICommand? _command; + private WeakEventListener? _commandListener; + private string _title = string.Empty; + + public virtual IIconInfo? Icon + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(Icon)); + } + } + + public virtual string Title + { + get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty; + + set + { + _title = value; + OnPropertyChanged(nameof(Title)); + } + } + + public virtual string Subtitle + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Subtitle)); + } + } + += string.Empty; + + public virtual ICommand? Command + { + get => _command; + set + { + if (_commandListener is not null) + { + _commandListener.Detach(); + _commandListener = null; + } + + _command = value; + + if (value is not null) + { + _commandListener = new(this, OnCommandPropertyChanged, listener => value.PropChanged -= listener.OnEvent); + value.PropChanged += _commandListener.OnEvent; + } + + OnPropertyChanged(nameof(Command)); + if (string.IsNullOrEmpty(_title)) + { + OnPropertyChanged(nameof(Title)); + } + } + } + + private void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args) + { + // command's name affects Title only if Title wasn't explicitly set + if (args.PropertyName == nameof(ICommand.Name) && string.IsNullOrEmpty(_title)) + { + instance.OnPropertyChanged(nameof(Title)); + } + } + + public virtual IContextItem[] MoreCommands + { + get; + set + { + field = value; + OnPropertyChanged(nameof(MoreCommands)); + } + } + += []; + + public CommandItem() + : this(new NoOpCommand()) + { + } + + public CommandItem(ICommand command) + { + Command = command; + } + + public CommandItem(ICommandItem other) + { + Command = other.Command; + Subtitle = other.Subtitle; + Icon = (IconInfo?)other.Icon; + MoreCommands = other.MoreCommands; + } + + public CommandItem( + string title, + string subtitle = "", + string name = "", + Action? action = null, + ICommandResult? result = null) + { + var c = new AnonymousCommand(action); + if (!string.IsNullOrEmpty(name)) + { + c.Name = name; + } + + if (result is not null) + { + c.Result = result; + } + + Command = c; + + Title = title; + Subtitle = subtitle; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs new file mode 100644 index 0000000000..ca64c87b23 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2 +{ + public virtual string Id { get; protected set; } = string.Empty; + + public virtual string DisplayName { get; protected set; } = string.Empty; + + public virtual IconInfo Icon { get; protected set; } = new IconInfo(); + + public event TypedEventHandler? ItemsChanged; + + public abstract ICommandItem[] TopLevelCommands(); + + public virtual IFallbackCommandItem[]? FallbackCommands() => null; + + public virtual ICommand? GetCommand(string id) => null; + + public virtual ICommandSettings? Settings { get; protected set; } + + public virtual bool Frozen { get; protected set; } = true; + + IIconInfo ICommandProvider.Icon => Icon; + + public virtual void InitializeWithHost(IExtensionHost host) => ExtensionHost.Initialize(host); + +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + public virtual void Dispose() + { + } +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize + + protected void RaiseItemsChanged(int totalItems = -1) + { + try + { + // TODO #181 - This is the same thing that BaseObservable has to deal with. + ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems)); + } + catch + { + } + } + + /// + /// This is used to manually populate the WinRT type cache in CmdPal with + /// any interfaces that might not follow a straight linear path of requires. + /// + /// You don't need to call this as an extension author. + /// + /// an array of objects that implement all the leaf interfaces we support + public object[] GetApiExtensionStubs() + { + return [new SupportCommandsWithProperties()]; + } + + /// + /// A stub class which implements IExtendedAttributesProvider. Just marshalling this + /// across the ABI will be enough for CmdPal to store IExtendedAttributesProvider in + /// its type cache. + /// + private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider + { + public IDictionary? GetProperties() => null; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandResult.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandResult.cs new file mode 100644 index 0000000000..4be5f5092b --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/CommandResult.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class CommandResult : ICommandResult +{ + public ICommandResultArgs? Args { get; private set; } + + public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss; + + public static CommandResult Dismiss() + { + return new CommandResult() + { + Kind = CommandResultKind.Dismiss, + }; + } + + public static CommandResult GoHome() + { + return new CommandResult() + { + Kind = CommandResultKind.GoHome, + Args = null, + }; + } + + public static CommandResult GoBack() + { + return new CommandResult() + { + Kind = CommandResultKind.GoBack, + Args = null, + }; + } + + public static CommandResult Hide() + { + return new CommandResult() + { + Kind = CommandResultKind.Hide, + Args = null, + }; + } + + public static CommandResult KeepOpen() + { + return new CommandResult() + { + Kind = CommandResultKind.KeepOpen, + Args = null, + }; + } + + public static CommandResult GoToPage(GoToPageArgs args) + { + return new CommandResult() + { + Kind = CommandResultKind.GoToPage, + Args = args, + }; + } + + public static CommandResult ShowToast(ToastArgs args) + { + return new CommandResult() + { + Kind = CommandResultKind.ShowToast, + Args = args, + }; + } + + public static CommandResult ShowToast(string message) + { + return new CommandResult() + { + Kind = CommandResultKind.ShowToast, + Args = new ToastArgs() { Message = message }, + }; + } + + public static CommandResult Confirm(ConfirmationArgs args) + { + return new CommandResult() + { + Kind = CommandResultKind.Confirm, + Args = args, + }; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ConfirmableCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ConfirmableCommand.cs new file mode 100644 index 0000000000..c2748b6f6a --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ConfirmableCommand.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Common.Commands; + +public sealed partial class ConfirmableCommand : InvokableCommand +{ + private readonly IInvokableCommand? _command; + + public Func? IsConfirmationRequired { get; init; } + + public required string ConfirmationTitle { get; init; } + + public required string ConfirmationMessage { get; init; } + + public required IInvokableCommand Command + { + get => _command!; + init + { + if (_command is INotifyPropChanged oldNotifier) + { + oldNotifier.PropChanged -= InnerCommand_PropChanged; + } + + _command = value; + + if (_command is INotifyPropChanged notifier) + { + notifier.PropChanged += InnerCommand_PropChanged; + } + + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(Id)); + OnPropertyChanged(nameof(Icon)); + } + } + + public override string Name + { + get => (_command as Command)?.Name ?? base.Name; + set + { + if (_command is Command cmd) + { + cmd.Name = value; + } + else + { + base.Name = value; + } + } + } + + public override string Id + { + get => (_command as Command)?.Id ?? base.Id; + set + { + var previous = Id; + if (_command is Command cmd) + { + cmd.Id = value; + } + else + { + base.Id = value; + } + + if (previous != Id) + { + OnPropertyChanged(nameof(Id)); + } + } + } + + public override IconInfo Icon + { + get => (_command as Command)?.Icon ?? base.Icon; + set + { + if (_command is Command cmd) + { + cmd.Icon = value; + } + else + { + base.Icon = value; + } + } + } + + public ConfirmableCommand() + { + // Allow init-only construction + } + + [SetsRequiredMembers] + public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func? isConfirmationRequired = null) + { + ArgumentNullException.ThrowIfNull(command); + ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage); + ArgumentNullException.ThrowIfNull(confirmationMessage); + + IsConfirmationRequired = isConfirmationRequired; + ConfirmationTitle = confirmationTitle; + ConfirmationMessage = confirmationMessage; + Command = command; + } + + private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args) + { + var property = args.PropertyName; + + if (string.IsNullOrEmpty(property) || property == nameof(Name)) + { + OnPropertyChanged(nameof(Name)); + } + + if (string.IsNullOrEmpty(property) || property == nameof(Id)) + { + OnPropertyChanged(nameof(Id)); + } + + if (string.IsNullOrEmpty(property) || property == nameof(Icon)) + { + OnPropertyChanged(nameof(Icon)); + } + } + + public override ICommandResult Invoke() + { + var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true; + if (showConfirmationDialog) + { + return CommandResult.Confirm(new ConfirmationArgs + { + Title = ConfirmationTitle, + Description = ConfirmationMessage, + PrimaryCommand = Command, + IsPrimaryCommandCritical = true, + }); + } + else + { + return Command.Invoke(this) ?? CommandResult.Dismiss(); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyPathCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyPathCommand.cs new file mode 100644 index 0000000000..9dcc8f36fb --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyPathCommand.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.Globalization; +using System.Text; +using Microsoft.CommandPalette.Extensions.Toolkit.Properties; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class CopyPathCommand : InvokableCommand +{ + internal static IconInfo CopyPath { get; } = new("\uE8c8"); // Copy + + private static readonly CompositeFormat CopyFailedFormat = CompositeFormat.Parse(Resources.copy_failed); + + private readonly string _path; + + public CommandResult Result { get; set; } = CommandResult.ShowToast(Resources.CopyPathTextCommand_Result); + + public CopyPathCommand(string fullPath) + { + this._path = fullPath; + this.Name = Resources.CopyPathTextCommand_Name; + this.Icon = CopyPath; + } + + public override CommandResult Invoke() + { + try + { + ClipboardHelper.SetText(_path); + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage("Copy failed: " + ex.Message) { State = MessageState.Error }); + return CommandResult.ShowToast( + new ToastArgs + { + Message = string.Format(CultureInfo.CurrentCulture, CopyFailedFormat, ex.Message), + Result = CommandResult.KeepOpen(), + }); + } + + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyTextCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyTextCommand.cs new file mode 100644 index 0000000000..ed24bd01a7 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/CopyTextCommand.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class CopyTextCommand : InvokableCommand +{ + public virtual string Text { get; set; } + + public virtual CommandResult Result { get; set; } = CommandResult.ShowToast(Properties.Resources.CopyTextCommand_CopiedToClipboard); + + public CopyTextCommand(string text) + { + Text = text; + Name = Properties.Resources.CopyTextCommand_Copy; + Icon = new IconInfo("\uE8C8"); + } + + public override ICommandResult Invoke() + { + ClipboardHelper.SetText(Text); + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/NoOpCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/NoOpCommand.cs new file mode 100644 index 0000000000..7fd1d00fdc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/NoOpCommand.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.CommandPalette.Extensions.Toolkit; + +public partial class NoOpCommand : InvokableCommand +{ + public override ICommandResult Invoke() => CommandResult.KeepOpen(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenFileCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenFileCommand.cs new file mode 100644 index 0000000000..192d6313fc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenFileCommand.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 Microsoft.CommandPalette.Extensions.Toolkit.Properties; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class OpenFileCommand : InvokableCommand +{ + internal static IconInfo OpenFile { get; } = new("\uE8E5"); // OpenFile + + private readonly string _fullPath; + + public CommandResult Result { get; set; } = CommandResult.Dismiss(); + + public OpenFileCommand(string fullPath) + { + this._fullPath = fullPath; + this.Name = Resources.OpenFileCommand_Name; + this.Icon = OpenFile; + } + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.FileName = _fullPath; + process.StartInfo.UseShellExecute = true; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + ExtensionHost.LogMessage($"Unable to open {_fullPath}\n{ex}"); + } + } + + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenInConsoleCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenInConsoleCommand.cs new file mode 100644 index 0000000000..ff655387e0 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenInConsoleCommand.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.ComponentModel; +using System.Diagnostics; +using Microsoft.CommandPalette.Extensions.Toolkit.Properties; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class OpenInConsoleCommand : InvokableCommand +{ + internal static IconInfo OpenInConsoleIcon { get; } = new("\uE756"); // "CommandPrompt" + + private readonly string _path; + private bool _isDirectory; + + public OpenInConsoleCommand(string fullPath) + { + this._path = fullPath; + this.Name = Resources.OpenInConsoleCommand_Name; + this.Icon = OpenInConsoleIcon; + } + + public static OpenInConsoleCommand FromDirectory(string directory) => new(directory) { _isDirectory = true }; + + public static OpenInConsoleCommand FromFile(string file) => new(file); + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = _isDirectory ? _path : Path.GetDirectoryName(_path); + process.StartInfo.FileName = "cmd.exe"; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + ExtensionHost.LogMessage(new LogMessage($"Unable to open '{_path}'\n{ex.Message}\n{ex.StackTrace}") { State = MessageState.Error }); + } + } + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenPropertiesCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenPropertiesCommand.cs new file mode 100644 index 0000000000..171a6c9910 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenPropertiesCommand.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 System.Runtime.InteropServices; +using ManagedCsWin32; +using Microsoft.CommandPalette.Extensions.Toolkit.Properties; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class OpenPropertiesCommand : InvokableCommand +{ + internal static IconInfo OpenPropertiesIcon { get; } = new("\uE90F"); + + private readonly string _path; + + private static unsafe bool ShowFileProperties(string filename) + { + var propertiesPtr = Marshal.StringToHGlobalUni("properties"); + var filenamePtr = Marshal.StringToHGlobalUni(filename); + + try + { + var info = new Shell32.SHELLEXECUTEINFOW + { + CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW), + LpVerb = propertiesPtr, + LpFile = filenamePtr, + Show = (int)SHOW_WINDOW_CMD.SW_SHOW, + FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST, + }; + + return Shell32.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(propertiesPtr); + } + } + + public OpenPropertiesCommand(string fullPath) + { + this._path = fullPath; + this.Name = Resources.OpenPropertiesCommand_Name; + this.Icon = OpenPropertiesIcon; + } + + public override CommandResult Invoke() + { + try + { + ShowFileProperties(_path); + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage($"Error showing file properties '{_path}'\n{ex.Message}\n{ex.StackTrace}") { State = MessageState.Error }); + } + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenUrlCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenUrlCommand.cs new file mode 100644 index 0000000000..8078a258de --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenUrlCommand.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class OpenUrlCommand : InvokableCommand +{ + private readonly string _target; + + public CommandResult Result { get; set; } = CommandResult.KeepOpen(); + + public OpenUrlCommand(string target) + { + _target = target; + Name = Properties.Resources.OpenUrlCommand_Open; + Icon = new IconInfo("\uE8A7"); + } + + public override CommandResult Invoke() + { + ShellHelpers.OpenInShell(_target); + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenWithCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenWithCommand.cs new file mode 100644 index 0000000000..5cd11f8635 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/OpenWithCommand.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.Runtime.InteropServices; +using ManagedCsWin32; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.CommandPalette.Extensions.Toolkit.Properties; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.Core.Common.Commands; + +public partial class OpenWithCommand : InvokableCommand +{ + internal static IconInfo OpenWithIcon { get; } = new("\uE7AC"); + + private readonly string _path; + + private static unsafe bool OpenWith(string filename) + { + var filenamePtr = Marshal.StringToHGlobalUni(filename); + var verbPtr = Marshal.StringToHGlobalUni("openas"); + + try + { + var info = new Shell32.SHELLEXECUTEINFOW + { + CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW), + LpVerb = verbPtr, + LpFile = filenamePtr, + Show = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL, + FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST, + }; + + return Shell32.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(verbPtr); + } + } + + public OpenWithCommand(string fullPath) + { + this._path = fullPath; + this.Name = Resources.OpenWithCommand_Name; + this.Icon = OpenWithIcon; + } + + public override CommandResult Invoke() + { + OpenWith(_path); + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ShowFileInFolderCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ShowFileInFolderCommand.cs new file mode 100644 index 0000000000..72f703ccb9 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Commands/ShowFileInFolderCommand.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.Diagnostics; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ShowFileInFolderCommand : InvokableCommand +{ + private readonly string _path; + private static readonly IconInfo Ico = new("\uE838"); + + public CommandResult Result { get; set; } = CommandResult.Dismiss(); + + public ShowFileInFolderCommand(string path) + { + _path = path; + Name = Properties.Resources.ShowFileInFolderCommand_ShowInFolder; + Icon = Ico; + } + + public override CommandResult Invoke() + { + if (Path.Exists(_path)) + { + try + { + var argument = "/select, \"" + _path + "\""; + Process.Start("explorer.exe", argument); + } + catch (Exception) + { + } + } + + return Result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ConfirmationArgs.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ConfirmationArgs.cs new file mode 100644 index 0000000000..ec47e65719 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ConfirmationArgs.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ConfirmationArgs : IConfirmationArgs +{ + public virtual string? Title { get; set; } + + public virtual string? Description { get; set; } + + public virtual ICommand? PrimaryCommand { get; set; } + + public virtual bool IsPrimaryCommandCritical { get; set; } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ContentPage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ContentPage.cs new file mode 100644 index 0000000000..1d2670d91d --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ContentPage.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract partial class ContentPage : Page, IContentPage +{ + public event TypedEventHandler? ItemsChanged; + + public virtual IDetails? Details + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(Details)); + } + } + + public virtual IContextItem[] Commands { get; set; } = []; + + public abstract IContent[] GetContent(); + + protected void RaiseItemsChanged(int totalItems = -1) + { + try + { + // TODO #181 - This is the same thing that BaseObservable has to deal with. + ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems)); + } + catch + { + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Details.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Details.cs new file mode 100644 index 0000000000..f466f2fd71 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Details.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class Details : BaseObservable, IDetails +{ + public virtual IIconInfo HeroImage + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(HeroImage)); + } + } + += new IconInfo(); + + public virtual string Title + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Title)); + } + } + += string.Empty; + + public virtual string Body + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Body)); + } + } + += string.Empty; + + public virtual IDetailsElement[] Metadata + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Metadata)); + } + } + += []; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsCommands.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsCommands.cs new file mode 100644 index 0000000000..32b11faad4 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsCommands.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.CommandPalette.Extensions.Toolkit; + +public partial class DetailsCommands : IDetailsCommands +{ + public ICommand[]? Commands { get; set; } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsElement.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsElement.cs new file mode 100644 index 0000000000..4fcf08fbcc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsElement.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.CommandPalette.Extensions.Toolkit; + +public partial class DetailsElement : IDetailsElement +{ + public virtual string Key { get; set; } = string.Empty; + + public virtual IDetailsData? Data { get; set; } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsLink.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsLink.cs new file mode 100644 index 0000000000..8b72491b92 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsLink.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.CommandPalette.Extensions.Toolkit; + +public partial class DetailsLink : IDetailsLink +{ + public virtual Uri? Link { get; set; } + + public virtual string Text { get; set; } = string.Empty; + + public DetailsLink() + { + } + + public DetailsLink(string url) + : this(url, url) + { + } + + public DetailsLink(string url, string text) + { + if (Uri.TryCreate(url, default(UriCreationOptions), out var newUri)) + { + Link = newUri; + } + + Text = text; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsSeparator.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsSeparator.cs new file mode 100644 index 0000000000..654340ab43 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsSeparator.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.CommandPalette.Extensions.Toolkit; + +public partial class DetailsSeparator : IDetailsSeparator +{ +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsTags.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsTags.cs new file mode 100644 index 0000000000..a6d2e57715 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DetailsTags.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.CommandPalette.Extensions.Toolkit; + +public partial class DetailsTags : IDetailsTags +{ + public ITag[] Tags { get; set; } = []; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DynamicListPage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DynamicListPage.cs new file mode 100644 index 0000000000..ec2602fb56 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/DynamicListPage.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.CommandPalette.Extensions.Toolkit; + +public abstract class DynamicListPage : ListPage, IDynamicListPage +{ + public override string SearchText + { + get => base.SearchText; + set + { + var oldSearch = base.SearchText; + SetSearchNoUpdate(value); + UpdateSearchText(oldSearch, value); + } + } + + public abstract void UpdateSearchText(string oldSearch, string newSearch); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionHost.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionHost.cs new file mode 100644 index 0000000000..cc9e2af15f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionHost.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ExtensionHost +{ + public static IExtensionHost? Host { get; private set; } + + public static 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 static void LogMessage(ILogMessage message) + { + if (Host is not null) + { + _ = Task.Run(async () => + { + try + { + await Host.LogMessage(message); + } + catch (Exception) + { + } + }); + } + } + + public static void LogMessage(string message) + { + var logMessage = new LogMessage() { Message = message }; + LogMessage(logMessage); + } + + public static void ShowStatus(IStatusMessage message, StatusContext context) + { + if (Host is not null) + { + _ = Task.Run(async () => + { + try + { + await Host.ShowStatus(message, context); + } + catch (Exception) + { + } + }); + } + } + + public static void HideStatus(IStatusMessage message) + { + if (Host is not null) + { + _ = Task.Run(async () => + { + try + { + await Host.HideStatus(message); + } + catch (Exception) + { + } + }); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionInstanceManager`1.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionInstanceManager`1.cs new file mode 100644 index 0000000000..b80742f8f7 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionInstanceManager`1.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. + +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Windows.Win32; +using Windows.Win32.Foundation; +using WinRT; + +namespace Microsoft.CommandPalette.Extensions; + +[ComVisible(true)] +[GeneratedComClass] +internal sealed partial class ExtensionInstanceManager : IClassFactory +{ +#pragma warning disable SA1310 // Field names should not contain underscore + + private const int E_NOINTERFACE = unchecked((int)0x80004002); + + private const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110); + + private const int E_ACCESSDENIED = unchecked((int)0x80070005); + + // Known constant ignored by win32metadata and cswin32 projections. + // https://github.com/microsoft/win32metadata/blob/main/generation/WinSDK/RecompiledIdlHeaders/um/processthreadsapi.h + private static readonly HANDLE CURRENT_THREAD_PSEUDO_HANDLE = (HANDLE)(IntPtr)(-6); + + private static readonly Guid IID_IUnknown = Guid.Parse("00000000-0000-0000-C000-000000000046"); + +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly Func _createExtension; + + private readonly bool _restrictToMicrosoftExtensionHosts; + + private readonly Guid _clsid; + + public ExtensionInstanceManager(Func createExtension, bool restrictToMicrosoftExtensionHosts, Guid clsid) + { + _createExtension = createExtension; + _restrictToMicrosoftExtensionHosts = restrictToMicrosoftExtensionHosts; + _clsid = clsid; + } + + public void CreateInstance( + [MarshalAs(UnmanagedType.Interface)] object pUnkOuter, + Guid riid, + out IntPtr ppvObject) + { + if (_restrictToMicrosoftExtensionHosts && !IsMicrosoftExtensionHost()) + { + Marshal.ThrowExceptionForHR(E_ACCESSDENIED); + } + + ppvObject = IntPtr.Zero; + + if (pUnkOuter is not null) + { + Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION); + } + + if (riid == _clsid || riid == IID_IUnknown) + { + // Create the instance of the .NET object + var managed = _createExtension(); + var ins = MarshalInspectable.FromManaged(managed); + ppvObject = ins; + } + else + { + // The object that ppvObject points to does not support the + // interface identified by riid. + Marshal.ThrowExceptionForHR(E_NOINTERFACE); + } + } + + public void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock) + { + } + + private unsafe bool IsMicrosoftExtensionHost() + { + if (PInvoke.CoImpersonateClient() != 0) + { + return false; + } + + uint buffer = 0; + if (PInvoke.GetPackageFamilyNameFromToken(CURRENT_THREAD_PSEUDO_HANDLE, &buffer, null) != WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) + { + return false; + } + + var value = new char[buffer]; + fixed (char* p = value) + { + if (PInvoke.GetPackageFamilyNameFromToken(CURRENT_THREAD_PSEUDO_HANDLE, &buffer, p) != 0) + { + return false; + } + } + + if (PInvoke.CoRevertToSelf() != 0) + { + return false; + } + + var valueStr = new string(value); + return valueStr switch + { + "Microsoft.Windows.CmdPal_8wekyb3d8bbwe\0" or "Microsoft.Windows.CmdPal.Canary_8wekyb3d8bbwe\0" or "Microsoft.Windows.CmdPal.Dev_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome.Canary_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome.Dev_8wekyb3d8bbwe\0" or "Microsoft.WindowsTerminal\0" or "Microsoft.WindowsTerminal_8wekyb3d8bbwe\0" or "WindowsTerminalDev_8wekyb3d8bbwe\0" or "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\0" => true, + _ => false, + }; + } +} + +// https://docs.microsoft.com/windows/win32/api/unknwn/nn-unknwn-iclassfactory +[GeneratedComInterface] +[Guid("00000001-0000-0000-C000-000000000046")] +internal partial interface IClassFactory +{ + void CreateInstance( + [MarshalAs(UnmanagedType.Interface)] object pUnkOuter, + Guid riid, + out IntPtr ppvObject); + + void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionServer.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionServer.cs new file mode 100644 index 0000000000..2f37d9d64f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ExtensionServer.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.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Microsoft.CommandPalette.Extensions; + +public sealed partial class ExtensionServer : IDisposable +{ + private readonly HashSet registrationCookies = []; + private ExtensionInstanceManager? _extensionInstanceManager; + private ComWrappers? _comWrappers; + + public void RegisterExtension(Func createExtension, bool restrictToMicrosoftExtensionHosts = false) + where T : IExtension + { + Trace.WriteLine($"Registering class object:"); + Trace.Indent(); + Trace.WriteLine($"CLSID: {typeof(T).GUID:B}"); + Trace.WriteLine($"Type: {typeof(T)}"); + + int cookie; + var clsid = typeof(T).GUID; + var wrappedCallback = () => (IExtension)createExtension(); + _extensionInstanceManager ??= new ExtensionInstanceManager(wrappedCallback, restrictToMicrosoftExtensionHosts, typeof(T).GUID); + _comWrappers ??= new StrategyBasedComWrappers(); + + var f = _comWrappers.GetOrCreateComInterfaceForObject(_extensionInstanceManager, CreateComInterfaceFlags.None); + + var hr = Ole32.CoRegisterClassObject( + ref clsid, + f, + Ole32.CLSCTX_LOCAL_SERVER, + Ole32.REGCLS_MULTIPLEUSE | Ole32.REGCLS_SUSPENDED, + out cookie); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + registrationCookies.Add(cookie); + Trace.WriteLine($"Cookie: {cookie}"); + Trace.Unindent(); + + hr = Ole32.CoResumeClassObjects(); + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + +#pragma warning disable CA1822 // Mark members as static + public void Run() => + + // TODO : We need to handle lifetime management of the server. + // For details around ref counting and locking of out-of-proc COM servers, see + // https://docs.microsoft.com/windows/win32/com/out-of-process-server-implementation-helpers + Console.ReadLine(); + + public void Dispose() + { + Trace.WriteLine($"Revoking class object registrations:"); + Trace.Indent(); + foreach (var cookie in registrationCookies) + { + Trace.WriteLine($"Cookie: {cookie}"); + var hr = Ole32.CoRevokeClassObject(cookie); + Debug.Assert(hr >= 0, $"CoRevokeClassObject failed ({hr:x}). Cookie: {cookie}"); + } + + Trace.Unindent(); + } + + private sealed class Ole32 + { +#pragma warning disable SA1310 // Field names should not contain underscore + // https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx + public const int CLSCTX_LOCAL_SERVER = 0x4; + + // https://docs.microsoft.com/windows/win32/api/combaseapi/ne-combaseapi-regcls + public const int REGCLS_MULTIPLEUSE = 1; + public const int REGCLS_SUSPENDED = 4; +#pragma warning restore SA1310 // Field names should not contain underscore + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coregisterclassobject + [DllImport(nameof(Ole32))] + public static extern int CoRegisterClassObject(ref Guid guid, IntPtr obj, int context, int flags, out int register); + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coresumeclassobjects + [DllImport(nameof(Ole32))] + public static extern int CoResumeClassObjects(); + + // https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-corevokeclassobject + [DllImport(nameof(Ole32))] + public static extern int CoRevokeClassObject(int register); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs new file mode 100644 index 0000000000..1c0b5c18d0 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IFallbackHandler +{ + private readonly IFallbackHandler? _fallbackHandler; + + public FallbackCommandItem(string displayTitle) + { + DisplayTitle = displayTitle; + } + + public FallbackCommandItem(ICommand command, string displayTitle) + : base(command) + { + DisplayTitle = displayTitle; + if (command is IFallbackHandler f) + { + _fallbackHandler = f; + } + } + + public IFallbackHandler? FallbackHandler + { + get => _fallbackHandler ?? this; + init => _fallbackHandler = value; + } + + public virtual string DisplayTitle { get; } + + public virtual void UpdateQuery(string query) => _fallbackHandler?.UpdateQuery(query); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filter.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filter.cs new file mode 100644 index 0000000000..961556e572 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filter.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class Filter : BaseObservable, IFilter +{ + public virtual IIconInfo Icon + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(Icon)); + } + } + += new IconInfo(); + + public virtual string Id + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Id)); + } + } + += string.Empty; + + public virtual string Name + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Name)); + } + } + += string.Empty; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs new file mode 100644 index 0000000000..2379382d1e --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Filters.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract partial class Filters : BaseObservable, IFilters +{ + public string CurrentFilterId + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(CurrentFilterId)); + } + } + + = string.Empty; + + // This method should be overridden in derived classes to provide the actual filters. + public abstract IFilterItem[] GetFilters(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs new file mode 100644 index 0000000000..7e12d38d0c --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.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 Windows.Foundation.Collections; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +/// +/// Represents an icon that is a font glyph. +/// This is used for icons that are defined by a specific font face, +/// such as Wingdings. +/// +/// Note that Command Palette will default to using the Segoe Fluent Icons, +/// Segoe MDL2 Assets font for glyphs in the Segoe UI Symbol range, or Segoe +/// UI for any other glyphs. This class is only needed if you want a non-Segoe +/// font icon. +/// +public partial class FontIconData : IconData, IExtendedAttributesProvider +{ + public string FontFamily { get; set; } + + public FontIconData(string glyph, string fontFamily) + : base(glyph) + { + FontFamily = fontFamily; + } + + public IDictionary? GetProperties() => new ValueSet() + { + { "FontFamily", FontFamily }, + }; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FormContent.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FormContent.cs new file mode 100644 index 0000000000..7a8d400a89 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FormContent.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class FormContent : BaseObservable, IFormContent +{ + public virtual string DataJson + { + get; + set + { + field = value; + OnPropertyChanged(nameof(DataJson)); + } + } + += string.Empty; + + public virtual string StateJson + { + get; + set + { + field = value; + OnPropertyChanged(nameof(StateJson)); + } + } + += string.Empty; + + public virtual string TemplateJson + { + get; + set + { + field = value; + OnPropertyChanged(nameof(TemplateJson)); + } + } + += string.Empty; + + public virtual ICommandResult SubmitForm(string inputs, string data) => SubmitForm(inputs); + + public virtual ICommandResult SubmitForm(string inputs) => CommandResult.KeepOpen(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs new file mode 100644 index 0000000000..f4591bc443 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft 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.CommandPalette.Extensions.Toolkit; + +// Inspired by the fuzzy.rs from edit.exe +public static class FuzzyStringMatcher +{ + private const int NOMATCH = 0; + + public static int ScoreFuzzy(string needle, string haystack, bool allowNonContiguousMatches = true) + { + var (s, _) = ScoreFuzzyWithPositions(needle, haystack, allowNonContiguousMatches); + return s; + } + + public static (int Score, List Positions) ScoreFuzzyWithPositions(string needle, string haystack, bool allowNonContiguousMatches) + { + if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(needle)) + { + return (NOMATCH, new List()); + } + + var target = haystack.ToCharArray(); + var query = needle.ToCharArray(); + + if (target.Length < query.Length) + { + return (NOMATCH, new List()); + } + + var targetUpper = FoldCase(haystack); + var queryUpper = FoldCase(needle); + var targetUpperChars = targetUpper.ToCharArray(); + var queryUpperChars = queryUpper.ToCharArray(); + + var area = query.Length * target.Length; + var scores = new int[area]; + var matches = new int[area]; + + for (var qi = 0; qi < query.Length; qi++) + { + var qiOffset = qi * target.Length; + var qiPrevOffset = qi > 0 ? (qi - 1) * target.Length : 0; + + for (var ti = 0; ti < target.Length; ti++) + { + var currentIndex = qiOffset + ti; + var diagIndex = (qi > 0 && ti > 0) ? qiPrevOffset + ti - 1 : 0; + var leftScore = ti > 0 ? scores[currentIndex - 1] : 0; + var diagScore = (qi > 0 && ti > 0) ? scores[diagIndex] : 0; + var matchSeqLen = (qi > 0 && ti > 0) ? matches[diagIndex] : 0; + + var score = (diagScore == 0 && qi != 0) ? 0 : + ComputeCharScore( + query[qi], + queryUpperChars[qi], + ti != 0 ? target[ti - 1] : null, + target[ti], + targetUpperChars[ti], + matchSeqLen); + + var isValidScore = score != 0 && diagScore + score >= leftScore && + (allowNonContiguousMatches || qi > 0 || + targetUpperChars.Skip(ti).Take(queryUpperChars.Length).SequenceEqual(queryUpperChars)); + + if (isValidScore) + { + matches[currentIndex] = matchSeqLen + 1; + scores[currentIndex] = diagScore + score; + } + else + { + matches[currentIndex] = NOMATCH; + scores[currentIndex] = leftScore; + } + } + } + + var positions = new List(); + if (query.Length > 0 && target.Length > 0) + { + var qi = query.Length - 1; + var ti = target.Length - 1; + + while (true) + { + var index = (qi * target.Length) + ti; + if (matches[index] == NOMATCH) + { + if (ti == 0) + { + break; + } + + ti--; + } + else + { + positions.Add(ti); + if (qi == 0 || ti == 0) + { + break; + } + + qi--; + ti--; + } + } + + positions.Reverse(); + } + + return (scores[area - 1], positions); + } + + private static string FoldCase(string input) + { + return input.ToUpperInvariant(); + } + + private static int ComputeCharScore( + char query, + char queryLower, + char? targetPrev, + char targetCurr, + char targetLower, + int matchSeqLen) + { + if (!ConsiderAsEqual(queryLower, targetLower)) + { + return 0; + } + + var score = 1; // Character match bonus + + if (matchSeqLen > 0) + { + score += matchSeqLen * 5; // Consecutive match bonus + } + + if (query == targetCurr) + { + score += 1; // Same case bonus + } + + if (targetPrev.HasValue) + { + var sepBonus = ScoreSeparator(targetPrev.Value); + if (sepBonus > 0) + { + score += sepBonus; + } + else if (char.IsUpper(targetCurr) && matchSeqLen == 0) + { + score += 2; // CamelCase bonus + } + } + else + { + score += 8; // Start of word bonus + } + + return score; + } + + private static bool ConsiderAsEqual(char a, char b) + { + return a == b || (a == '/' && b == '\\') || (a == '\\' && b == '/'); + } + + private static int ScoreSeparator(char ch) + { + return ch switch + { + '/' or '\\' => 5, + '_' or '-' or '.' or ' ' or '\'' or '"' or ':' => 4, + _ => 0, + }; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GalleryGridLayout.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GalleryGridLayout.cs new file mode 100644 index 0000000000..0c696b8b16 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GalleryGridLayout.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout +{ + public virtual bool ShowTitle + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(ShowTitle)); + } + } + + = true; + + public virtual bool ShowSubtitle + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(ShowSubtitle)); + } + } + + = true; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GlobalSuppressions.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GlobalSuppressions.cs new file mode 100644 index 0000000000..9f29e23714 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation +// The 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; + +[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "~T:WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_object__global__Microsoft_CommandPalette_Extensions_IPropChangedEventArgs_.EventState")] +[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "~T:WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_object__global__Microsoft_CommandPalette_Extensions_IItemsChangedEventArgs_.EventState")] diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GoToPageArgs.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GoToPageArgs.cs new file mode 100644 index 0000000000..77ad2d2951 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/GoToPageArgs.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.CommandPalette.Extensions.Toolkit; + +public partial class GoToPageArgs : IGoToPageArgs +{ + public required string PageId { get; set; } + + public NavigationMode NavigationMode { get; set; } = NavigationMode.Push; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ISettingsForm.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ISettingsForm.cs new file mode 100644 index 0000000000..2f1e5ed37b --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ISettingsForm.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.Text.Json.Nodes; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +internal interface ISettingsForm +{ + public string ToForm(); + + public void Update(JsonObject payload); + + public Dictionary ToDictionary(); + + public string ToDataIdentifier(); + + public string ToState(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs new file mode 100644 index 0000000000..35fdf7b8e7 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconData.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.CodeAnalysis; +using Windows.Storage.Streams; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class IconData : IIconData +{ + public IRandomAccessStreamReference? Data { get; set; } + + public string? Icon { get; set; } = string.Empty; + + public IconData(string? icon) + { + Icon = icon; + } + + public IconData(IRandomAccessStreamReference data) + { + Data = data; + } + + internal IconData() + : this(string.Empty) + { + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconHelpers.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconHelpers.cs new file mode 100644 index 0000000000..07e974303f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconHelpers.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed class IconHelpers +{ + public static IconInfo FromRelativePath(string path) => new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), path)); + + public static IconInfo FromRelativePaths(string lightIcon, string darkIcon) => + new( + new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), lightIcon)), + new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), darkIcon))); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs new file mode 100644 index 0000000000..731b529903 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.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 Windows.Storage.Streams; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class IconInfo : IIconInfo +{ + public virtual IconData Dark { get; set; } = new(); + + public virtual IconData Light { get; set; } = new(); + + IIconData IIconInfo.Dark => Dark; + + IIconData IIconInfo.Light => Light; + + public IconInfo(string? icon) + { + Dark = Light = new(icon); + } + + public IconInfo(IconData light, IconData dark) + { + Light = light; + Dark = dark; + } + + public IconInfo(IconData icon) + { + Light = icon; + Dark = icon; + } + + internal IconInfo() + : this(string.Empty) + { + } + + public static IconInfo FromStream(IRandomAccessStream stream) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + return new IconInfo(data, data); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/InvokableCommand.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/InvokableCommand.cs new file mode 100644 index 0000000000..0d21a06730 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/InvokableCommand.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.CommandPalette.Extensions.Toolkit; + +public abstract partial class InvokableCommand : Command, IInvokableCommand +{ + public virtual ICommandResult Invoke() => CommandResult.KeepOpen(); + + public virtual ICommandResult Invoke(object? sender) => Invoke(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ItemsChangedEventArgs.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ItemsChangedEventArgs.cs new file mode 100644 index 0000000000..e859dd12cf --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ItemsChangedEventArgs.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ItemsChangedEventArgs : IItemsChangedEventArgs +{ + public int TotalItems { get; protected set; } + + public ItemsChangedEventArgs(int totalItems = -1) + { + TotalItems = totalItems; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs new file mode 100644 index 0000000000..98e2fae688 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.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.Text.Json.Serialization; +using static Microsoft.CommandPalette.Extensions.Toolkit.ChoiceSetSetting; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +[JsonSerializable(typeof(float))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(Choice))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")] +[JsonSerializable(typeof(List>))] +[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)] +internal sealed partial class JsonSerializationContext : JsonSerializerContext +{ +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSettingsManager.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSettingsManager.cs new file mode 100644 index 0000000000..09c1eebdbe --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/JsonSettingsManager.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.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract class JsonSettingsManager +{ + public Settings Settings { get; } = new(); + + public string FilePath { get; init; } = string.Empty; + + private static readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + }; + + public virtual void LoadSettings() + { + if (string.IsNullOrEmpty(FilePath)) + { + throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadSettings)}"); + } + + var filePath = FilePath; + if (!File.Exists(filePath)) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" }); + return; + } + + try + { + // Read the JSON content from the file + var jsonContent = File.ReadAllText(filePath); + + // Is it valid JSON? + if (JsonNode.Parse(jsonContent) is JsonObject savedSettings) + { + Settings.Update(jsonContent); + } + else + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Failed to parse settings file as JsonObject." }); + } + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + } + } + + public virtual void SaveSettings() + { + 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 = Settings.ToJson(); + + // 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 is not null ? item.Value.DeepClone() : null; + } + + var serialized = savedSettings.ToJsonString(_serializerOptions); + File.WriteAllText(FilePath, serialized); + } + else + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Failed to parse settings file as JsonObject." }); + } + } + else + { + ExtensionHost.LogMessage(new LogMessage() { Message = "Failed to parse settings file as JsonObject." }); + } + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/KeyChordHelpers.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/KeyChordHelpers.cs new file mode 100644 index 0000000000..0ffab0b7e4 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/KeyChordHelpers.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.System; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public static partial class KeyChordHelpers +{ + public static KeyChord FromModifiers( + bool ctrl = false, + bool alt = false, + bool shift = false, + bool win = false, + int vkey = 0, + int scanCode = 0) + { + var modifiers = (ctrl ? VirtualKeyModifiers.Control : VirtualKeyModifiers.None) + | (alt ? VirtualKeyModifiers.Menu : VirtualKeyModifiers.None) + | (shift ? VirtualKeyModifiers.Shift : VirtualKeyModifiers.None) + | (win ? VirtualKeyModifiers.Windows : VirtualKeyModifiers.None) + ; + return new(modifiers, vkey, scanCode); + } + + public static KeyChord FromModifiers( + bool ctrl = false, + bool alt = false, + bool shift = false, + bool win = false, + VirtualKey vkey = VirtualKey.None, + int scanCode = 0) + { + return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode); + } + + public static string FormatForDebug(KeyChord value) + { + var result = string.Empty; + + if (value.Modifiers.HasFlag(VirtualKeyModifiers.Control)) + { + result += "Ctrl+"; + } + + if (value.Modifiers.HasFlag(VirtualKeyModifiers.Shift)) + { + result += "Shift+"; + } + + if (value.Modifiers.HasFlag(VirtualKeyModifiers.Menu)) + { + result += "Alt+"; + } + + result += (VirtualKey)value.Vkey; + + return result; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs new file mode 100644 index 0000000000..3847ab8e55 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft 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.CommandPalette.Extensions.Toolkit; + +public partial class ListHelpers +{ + // Generate a score for a list item. + public static int ScoreListItem(string query, ICommandItem listItem) + { + if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query)) + { + return 1; + } + + if (string.IsNullOrEmpty(listItem.Title)) + { + return 0; + } + + var nameMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Title); + + // var locNameMatch = StringMatcher.FuzzySearch(query, NameLocalized); + var descriptionMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Subtitle); + + // var executableNameMatch = StringMatcher.FuzzySearch(query, ExePath); + // var locExecutableNameMatch = StringMatcher.FuzzySearch(query, ExecutableNameLocalized); + // var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName); + // var locLnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableNameLocalized); + // var score = new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, executableNameMatch.Score }.Max(); + return new[] { nameMatchScore, (descriptionMatchScore - 4) / 2, 0 }.Max(); + } + + public static IEnumerable FilterList(IEnumerable items, string query) + { + var scores = items + .Select(li => new ScoredListItem() { ListItem = li, Score = ScoreListItem(query, li) }) + .Where(score => score.Score > 0) + .OrderByDescending(score => score.Score); + return scores + .Select(score => score.ListItem); + } + + public static IEnumerable FilterList(IEnumerable items, string query, Func scoreFunction) + { + return FilterListWithScores(items, query, scoreFunction) + .Select(score => score.Item); + } + + public static IEnumerable> FilterListWithScores(IEnumerable items, string query, Func scoreFunction) + { + var scores = items + .Select(li => new Scored() { Item = li, Score = scoreFunction(query, li) }) + .Where(score => score.Score > 0) + .OrderByDescending(score => score.Score); + return scores; + } + + /// + /// Modifies the contents of `original` in-place, to match those of + /// `newContents`. The canonical use being: + /// ```cs + /// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn)); + /// ``` + /// + /// Any type that can be compared for equality + /// Collection to modify + /// The enumerable which `original` should match + public static void InPlaceUpdateList(IList original, IEnumerable newContents) + where T : class + { + InPlaceUpdateList(original, newContents, out _); + } + + /// + /// Modifies the contents of `original` in-place, to match those of + /// `newContents`. The canonical use being: + /// ```cs + /// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn)); + /// ``` + /// + /// Any type that can be compared for equality + /// Collection to modify + /// The enumerable which `original` should match + /// List of items that were removed from the original collection + public static void InPlaceUpdateList(IList original, IEnumerable newContents, out List removedItems) + where T : class + { + removedItems = []; + + // we're not changing newContents - stash this so we don't re-evaluate it every time + var numberOfNew = newContents.Count(); + + // Short circuit - new contents should just be empty + if (numberOfNew == 0) + { + removedItems.AddRange(original); + original.Clear(); + return; + } + + var i = 0; + foreach (var newItem in newContents) + { + if (i >= original.Count) + { + break; + } + + for (var j = i; j < original.Count; j++) + { + var og_2 = original[j]; + var areEqual_2 = og_2?.Equals(newItem) ?? false; + if (areEqual_2) + { + for (var k = i; k < j; k++) + { + // This item from the original list was not in the new list. Remove it. + removedItems.Add(original[i]); + original.RemoveAt(i); + } + + break; + } + } + + var og = original[i]; + var areEqual = og?.Equals(newItem) ?? false; + + // Is this new item already in the list? + if (areEqual) + { + // It is already in the list + } + else + { + // it isn't. Add it. + original.Insert(i, newItem); + } + + i++; + } + + // Remove any extra trailing items from the destination + while (original.Count > numberOfNew) + { + // RemoveAtEnd + removedItems.Add(original[original.Count - 1]); + original.RemoveAt(original.Count - 1); + } + + // Add any extra trailing items from the source + if (original.Count < numberOfNew) + { + var remaining = newContents.Skip(original.Count); + foreach (var item in remaining) + { + original.Add(item); + } + } + } +} + +public struct ScoredListItem +{ + public int Score; + public IListItem ListItem; +} + +public struct Scored +{ + public int Score; + public T Item; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs new file mode 100644 index 0000000000..ffd20643aa --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft 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.CommandPalette.Extensions.Toolkit; + +public partial class ListItem : CommandItem, IListItem +{ + private ITag[] _tags = []; + private IDetails? _details; + + private string _section = string.Empty; + private string _textToSuggest = string.Empty; + + public virtual ITag[] Tags + { + get => _tags; + set + { + _tags = value; + OnPropertyChanged(nameof(Tags)); + } + } + + public virtual IDetails? Details + { + get => _details; + set + { + _details = value; + OnPropertyChanged(nameof(Details)); + } + } + + public virtual string Section + { + get => _section; + set + { + _section = value; + OnPropertyChanged(nameof(Section)); + } + } + + public virtual string TextToSuggest + { + get => _textToSuggest; + set + { + _textToSuggest = value; + OnPropertyChanged(nameof(TextToSuggest)); + } + } + + public ListItem(ICommand command) + : base(command) + { + } + + public ListItem(ICommandItem command) + : base(command) + { + } + + public ListItem() + : base() + { + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListPage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListPage.cs new file mode 100644 index 0000000000..16992613d8 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ListPage.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ListPage : Page, IListPage +{ + private string _placeholderText = string.Empty; + private string _searchText = string.Empty; + private bool _showDetails; + private bool _hasMore; + private IFilters? _filters; + private IGridProperties? _gridProperties; + private ICommandItem? _emptyContent; + + public event TypedEventHandler? ItemsChanged; + + public virtual string PlaceholderText + { + get => _placeholderText; + set + { + _placeholderText = value; + OnPropertyChanged(nameof(PlaceholderText)); + } + } + + public virtual string SearchText + { + get => _searchText; + set + { + _searchText = value; + OnPropertyChanged(nameof(SearchText)); + } + } + + public virtual bool ShowDetails + { + get => _showDetails; + set + { + _showDetails = value; + OnPropertyChanged(nameof(ShowDetails)); + } + } + + public virtual bool HasMoreItems + { + get => _hasMore; + set + { + _hasMore = value; + OnPropertyChanged(nameof(HasMoreItems)); + } + } + + public virtual IFilters? Filters + { + get => _filters; + set + { + _filters = value; + OnPropertyChanged(nameof(Filters)); + } + } + + public virtual IGridProperties? GridProperties + { + get => _gridProperties; + set + { + _gridProperties = value; + OnPropertyChanged(nameof(GridProperties)); + } + } + + public virtual ICommandItem? EmptyContent + { + get => _emptyContent; + set + { + _emptyContent = value; + OnPropertyChanged(nameof(EmptyContent)); + } + } + + public virtual IListItem[] GetItems() => []; + + public virtual void LoadMore() + { + } + + protected void RaiseItemsChanged(int totalItems = -1) + { + try + { + // TODO #181 - This is the same thing that BaseObservable has to deal with. + ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems)); + } + catch + { + } + } + + protected void SetSearchNoUpdate(string newSearchText) + { + _searchText = newSearchText; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/LogMessage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/LogMessage.cs new file mode 100644 index 0000000000..97993c5a0c --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/LogMessage.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class LogMessage : BaseObservable, ILogMessage +{ + private MessageState _messageState = MessageState.Info; + + private string _message = string.Empty; + + public string Message + { + get => _message; + set + { + _message = value; + OnPropertyChanged(nameof(Message)); + } + } + + public MessageState State + { + get => _messageState; + set + { + _messageState = value; + OnPropertyChanged(nameof(State)); + } + } + + public LogMessage(string message = "") + { + _message = message; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ManagedCsWin32/Shell32.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ManagedCsWin32/Shell32.cs new file mode 100644 index 0000000000..de8c8e0bf3 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ManagedCsWin32/Shell32.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; + +namespace ManagedCsWin32; + +internal static partial class Shell32 +{ + [LibraryImport("SHELL32.dll", EntryPoint = "ShellExecuteExW", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool ShellExecuteEx(ref SHELLEXECUTEINFOW lpExecInfo); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SHELLEXECUTEINFOW + { + public uint CbSize; + public uint FMask; + public IntPtr Hwnd; + + public IntPtr LpVerb; + public IntPtr LpFile; + public IntPtr LpParameters; + public IntPtr LpDirectory; + public int Show; + public IntPtr HInstApp; + public IntPtr LpIDList; + public IntPtr LpClass; + public IntPtr HkeyClass; + public uint DwHotKey; + public IntPtr HIconOrMonitor; + public IntPtr Process; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MarkdownContent.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MarkdownContent.cs new file mode 100644 index 0000000000..b2de535d9a --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MarkdownContent.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class MarkdownContent : BaseObservable, IMarkdownContent +{ + public virtual string Body + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Body)); + } + } + += string.Empty; + + public MarkdownContent() + { + } + + public MarkdownContent(string body) + { + Body = body; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchOption.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchOption.cs new file mode 100644 index 0000000000..9f740e4ade --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchOption.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 System.Runtime.CompilerServices; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class MatchOption +{ + /// + /// Gets or sets prefix of match char, use for highlight + /// + [Obsolete("this is never used")] + public string Prefix { get; set; } = string.Empty; + + /// + /// Gets or sets suffix of match char, use for highlight + /// + [Obsolete("this is never used")] + public string Suffix { get; set; } = string.Empty; + + public bool IgnoreCase { get; set; } = true; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchResult.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchResult.cs new file mode 100644 index 0000000000..3848065f25 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MatchResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation +// The 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; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class MatchResult +{ + public MatchResult(bool success, SearchPrecisionScore searchPrecision) + { + Success = success; + SearchPrecision = searchPrecision; + } + + public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) + { + Success = success; + SearchPrecision = searchPrecision; + MatchData = matchData; + RawScore = rawScore; + } + + public bool Success { get; set; } + + /// + /// Gets the final score of the match result with search precision filters applied. + /// + public int Score { get; private set; } + + /// + /// The raw calculated search score without any search precision filtering applied. + /// + private int _rawScore; + + public int RawScore + { + get => _rawScore; + + set + { + _rawScore = value; + Score = ScoreAfterSearchPrecisionFilter(_rawScore); + } + } + + /// + /// Gets matched data to highlight. + /// + public List MatchData { get; private set; } = new(); + + public SearchPrecisionScore SearchPrecision { get; set; } + + public bool IsSearchPrecisionScoreMet() + { + return IsSearchPrecisionScoreMet(_rawScore); + } + + private bool IsSearchPrecisionScoreMet(int rawScore) + { + return rawScore >= (int)SearchPrecision; + } + + private int ScoreAfterSearchPrecisionFilter(int rawScore) + { + return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MediumGridLayout.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MediumGridLayout.cs new file mode 100644 index 0000000000..31359866f9 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/MediumGridLayout.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.CommandPalette.Extensions.Toolkit; + +public partial class MediumGridLayout : BaseObservable, IMediumGridLayout +{ + public virtual bool ShowTitle + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(ShowTitle)); + } + } + + = true; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj new file mode 100644 index 0000000000..f5f8f2ccbc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj @@ -0,0 +1,78 @@ + + + + + + + 10.0.26100.57 + + $(SolutionDir)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions.Toolkit + false + false + enable + enable + preview + + Microsoft.CommandPalette.Extensions.Toolkit.pri + AnyCPU + None + + + + true + true + $(MSBuildThisFileDirectory)..\..\..\..\..\.pipelines\272MSSharedLibSN2048.snk + + + + Microsoft.CommandPalette.Extensions + $(OutDir) + + + + + Guard + Spectre + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + True + True + + + IL2081;$(WarningsNotAsErrors) + + diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.cs new file mode 100644 index 0000000000..ccbe6ed885 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.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.Runtime.InteropServices; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +internal static partial class NativeMethods +{ + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags); + + [DllImport("shell32.dll", CharSet = CharSet.Auto)] + internal static extern IntPtr SHGetFileInfo(IntPtr pidl, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags); + + [DllImport("shell32.dll")] + internal static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); + + [DllImport("ole32.dll")] + internal static extern void CoTaskMemFree(IntPtr pv); + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] + internal static extern int SHLoadIndirectString(string pszSource, System.Text.StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct SHFILEINFO + { +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public IntPtr hIcon; + public int iIcon; + public uint dwAttributes; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string szDisplayName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] + public string szTypeName; +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter + } + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern bool DestroyIcon(IntPtr hIcon); + + [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] + internal static extern int SHGetImageList(int iImageList, ref Guid riid, out IntPtr ppv); + + [DllImport("comctl32.dll", SetLastError = true)] + internal static extern int ImageList_GetIcon(IntPtr himl, int i, int flags); + + [LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)] + internal static unsafe partial int AssocQueryStringW( + AssocF flags, + AssocStr str, + string pszAssoc, + string? pszExtra, + char* pszOut, + ref uint pcchOut); + + // SHDefExtractIconW lets us ask for specific sizes (incl. 256) + // nIconSize: HIWORD = large size, LOWORD = small size + [LibraryImport("shell32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)] + internal static partial int SHDefExtractIconW( + string pszIconFile, + int iIndex, + uint uFlags, + out nint phiconLarge, + out nint phiconSmall, + int nIconSize); + + [Flags] + public enum AssocF : uint + { + None = 0, + IsProtocol = 0x00001000, + } + + public enum AssocStr + { + Command = 1, + Executable, + FriendlyDocName, + FriendlyAppName, + NoOpen, + ShellNewValue, + DDECommand, + DDEIfExec, + DDEApplication, + DDETopic, + InfoTip, + QuickTip, + TileInfo, + ContentType, + DefaultIcon, + ShellExtension, + DropTarget, + DelegateExecute, + SupportedUriProtocols, + ProgId, + AppId, + AppPublisher, + AppIconReference, // sometimes present, but DefaultIcon is most common + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.txt b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.txt new file mode 100644 index 0000000000..21a724cd2d --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/NativeMethods.txt @@ -0,0 +1,11 @@ +CoImpersonateClient +GetCurrentThread +OpenThreadToken +GetPackageFamilyNameFromToken +CoRevertToSelf +SHGetKnownFolderPath +KNOWN_FOLDER_FLAG +GetCurrentPackageId + +SHOW_WINDOW_CMD +SEE_MASK_INVOKEIDLIST \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Page.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Page.cs new file mode 100644 index 0000000000..0dfd1d0bdd --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Page.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class Page : Command, IPage +{ + private bool _loading; + private string _title = string.Empty; + private OptionalColor _accentColor; + + public virtual bool IsLoading + { + get => _loading; + set + { + _loading = value; + OnPropertyChanged(nameof(IsLoading)); + } + } + + public virtual string Title + { + get => _title; + set + { + _title = value; + OnPropertyChanged(nameof(Title)); + } + } + + public virtual OptionalColor AccentColor + { + get => _accentColor; + set + { + _accentColor = value; + OnPropertyChanged(nameof(AccentColor)); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ProgressState.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ProgressState.cs new file mode 100644 index 0000000000..4cc7e5921b --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ProgressState.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ProgressState : BaseObservable, IProgressState +{ + private bool _isIndeterminate; + + private uint _progressPercent; + + public virtual bool IsIndeterminate + { + get => _isIndeterminate; + set + { + _isIndeterminate = value; + OnPropertyChanged(nameof(IsIndeterminate)); + } + } + + public virtual uint ProgressPercent + { + get => _progressPercent; + set + { + _progressPercent = value; + OnPropertyChanged(nameof(ProgressPercent)); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/PropChangedEventArgs.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/PropChangedEventArgs.cs new file mode 100644 index 0000000000..aab02590f3 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/PropChangedEventArgs.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class PropChangedEventArgs : IPropChangedEventArgs +{ + public string PropertyName { get; private set; } + + public PropChangedEventArgs(string propertyName) + { + PropertyName = propertyName; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..e2fd310a60 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/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.CommandPalette.Extensions.Toolkit.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.CommandPalette.Extensions.Toolkit.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 Invoke. + /// + internal static string AnonymousCommand_Invoke { + get { + return ResourceManager.GetString("AnonymousCommand_Invoke", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy failed ({0}). Please try again.. + /// + internal static string copy_failed { + get { + return ResourceManager.GetString("copy_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy path. + /// + internal static string CopyPathTextCommand_Name { + get { + return ResourceManager.GetString("CopyPathTextCommand_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied path to clipboard. + /// + internal static string CopyPathTextCommand_Result { + get { + return ResourceManager.GetString("CopyPathTextCommand_Result", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied to clipboard. + /// + internal static string CopyTextCommand_CopiedToClipboard { + get { + return ResourceManager.GetString("CopyTextCommand_CopiedToClipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy. + /// + internal static string CopyTextCommand_Copy { + get { + return ResourceManager.GetString("CopyTextCommand_Copy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + internal static string OpenFileCommand_Name { + get { + return ResourceManager.GetString("OpenFileCommand_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open path in console. + /// + internal static string OpenInConsoleCommand_Name { + get { + return ResourceManager.GetString("OpenInConsoleCommand_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties. + /// + internal static string OpenPropertiesCommand_Name { + get { + return ResourceManager.GetString("OpenPropertiesCommand_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + internal static string OpenUrlCommand_Open { + get { + return ResourceManager.GetString("OpenUrlCommand_Open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open with. + /// + internal static string OpenWithCommand_Name { + get { + return ResourceManager.GetString("OpenWithCommand_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + internal static string Settings { + get { + return ResourceManager.GetString("Settings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show in folder. + /// + internal static string ShowFileInFolderCommand_ShowInFolder { + get { + return ResourceManager.GetString("ShowFileInFolderCommand_ShowInFolder", resourceCulture); + } + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.resx b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.resx new file mode 100644 index 0000000000..40fdb2c813 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Properties/Resources.resx @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Invoke + + + Copy + + + Copied to clipboard + + + Copy path + + + Copied path to clipboard + + + Open + + + Settings + + + Show in folder + + + Open path in console + + + Properties + + + Open + + + Open with + + + Copy failed ({0}). Please try again. + {0} is the error message + + \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SearchPrecisionScore.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SearchPrecisionScore.cs new file mode 100644 index 0000000000..234fea3f3c --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SearchPrecisionScore.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.CommandPalette.Extensions.Toolkit; + +public enum SearchPrecisionScore +{ + Regular = 50, + Low = 20, + None = 0, +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs new file mode 100644 index 0000000000..d47eff6b22 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Separator.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.CommandPalette.Extensions.Toolkit; + +public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem +{ +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Setting`1.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Setting`1.cs new file mode 100644 index 0000000000..b487d30e58 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Setting`1.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.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract class Setting : ISettingsForm +{ + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; + + public T? Value { get; set; } + + public string Key { get; } + + public bool IsRequired { get; set; } + + public string ErrorMessage { get; set; } = string.Empty; + + public string Label { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + protected Setting() + { + Value = default; + Key = string.Empty; + } + + public Setting(string key, T defaultValue) + { + Key = key; + Value = defaultValue; + } + + public Setting(string key, string label, string description, T defaultValue) + { + Key = key; + Value = defaultValue; + Label = label; + Description = description; + } + + public abstract Dictionary ToDictionary(); + + public string ToDataIdentifier() => $"\"{Key}\": \"{Key}\""; + + public string ToForm() + { + var bodyJson = JsonSerializer.Serialize(ToDictionary(), JsonSerializationContext.Default.Dictionary); + var dataJson = $"\"{Key}\": \"{Key}\""; + + var json = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + {{bodyJson}} + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Save", + "data": { + {{dataJson}} + } + } + ] +} +"""; + return json; + } + + public abstract void Update(JsonObject payload); + + public abstract string ToState(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs new file mode 100644 index 0000000000..fbd74ce694 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Settings.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation +// The 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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed partial class Settings : ICommandSettings +{ + private readonly Dictionary _settings = []; + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; + + public event TypedEventHandler? SettingsChanged; + + public void Add(Setting s) => _settings.Add(s.Key, s); + + public T? GetSetting(string key) => _settings[key] is Setting s ? s.Value : default; + + public bool TryGetSetting(string key, out T? val) + { + object? o; + if (_settings.TryGetValue(key, out o)) + { + if (o is Setting s) + { + val = s.Value; + return true; + } + } + + val = default; + return false; + } + + internal string ToFormJson() + { + var settings = _settings + .Values + .Where(s => s is ISettingsForm) + .Select(s => s as ISettingsForm) + .Where(s => s is not null) + .Select(s => s!); + + var bodies = string.Join(",", settings + .Select(s => JsonSerializer.Serialize(s.ToDictionary(), JsonSerializationContext.Default.Dictionary))); + + var datas = string.Join(",", settings.Select(s => s.ToDataIdentifier())); + + var json = $$""" +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + {{bodies}} + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Save", + "data": { + {{datas}} + } + } + ] +} +"""; + return json; + } + + public string ToJson() + { + var settings = _settings + .Values + .Where(s => s is ISettingsForm) + .Select(s => s as ISettingsForm) + .Where(s => s is not null) + .Select(s => s!); + var content = string.Join(",\n", settings.Select(s => s.ToState())); + return $"{{\n{content}\n}}"; + } + + public void Update(string data) + { + var formInput = JsonNode.Parse(data)?.AsObject(); + if (formInput is null) + { + return; + } + + foreach (var key in _settings.Keys) + { + var value = _settings[key]; + if (value is ISettingsForm f) + { + f.Update(formInput); + } + } + } + + internal void RaiseSettingsChanged() + { + var handlers = SettingsChanged; + handlers?.Invoke(this, this); + } + + private sealed partial class SettingsContentPage : ContentPage + { + private readonly Settings _settings; + + public override IContent[] GetContent() => _settings.ToContent(); + + public SettingsContentPage(Settings settings) + { + _settings = settings; + Name = Properties.Resources.Settings; + Icon = new IconInfo("\uE713"); // Settings icon + + // When our settings change, make sure to let CmdPal know to + // retrieve the new forms + _settings.SettingsChanged += (s, e) => RaiseItemsChanged(); + } + } + + public IContentPage SettingsPage => new SettingsContentPage(this); + + public IContent[] ToContent() => [new SettingsForm(this)]; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.cs new file mode 100644 index 0000000000..2bab5e78dc --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SettingsForm.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.Nodes; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class SettingsForm : FormContent +{ + private readonly Settings _settings; + + internal SettingsForm(Settings settings) + { + _settings = settings; + TemplateJson = _settings.ToFormJson(); + } + + public override ICommandResult SubmitForm(string inputs, string data) + { + var formInput = JsonNode.Parse(inputs)?.AsObject(); + if (formInput is null) + { + return CommandResult.KeepOpen(); + } + + // Re-render the current value of the settings to a card. The + // SettingsContentPage will raise an ItemsChanged in its own + // SettingsChange handler, so we need to be prepared to return the + // current settings value. + TemplateJson = _settings.ToFormJson(); + + _settings.Update(inputs); + _settings.RaiseSettingsChanged(); + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs new file mode 100644 index 0000000000..3ddbcc5ad6 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation +// The 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.Diagnostics.CodeAnalysis; +using Microsoft.Win32; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public static class ShellHelpers +{ + /// + /// These are the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell, + /// Shell does not use PATHEXT, but has a magic fixed list. + /// + public static string[] ExecutableExtensions { get; } = [".PIF", ".COM", ".EXE", ".BAT", ".CMD"]; + + /// + /// Determines whether the specified file name represents an executable file + /// by examining its extension against the known list of Windows Shell + /// executable extensions (a fixed list that does not honor PATHEXT). + /// + /// The file name (with or without path) whose extension will be evaluated. + /// + /// True if the file name has an extension that matches one of the recognized executable + /// extensions; otherwise, false. Returns false for null, empty, or whitespace input. + /// + public static bool IsExecutableFile(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return false; + } + + var fileExtension = Path.GetExtension(fileName); + return IsExecutableExtension(fileExtension); + } + + /// + /// Determines whether the provided file extension (including the leading dot) + /// is one of the Windows Shell recognized executable extensions. + /// + /// The file extension to test. Should include the leading dot (e.g. ".exe"). + /// + /// True if the extension matches (case-insensitive) one of the known executable + /// extensions; false if it does not match or if the input is null/whitespace. + /// + public static bool IsExecutableExtension(string fileExtension) + { + if (string.IsNullOrWhiteSpace(fileExtension)) + { + // Shell won't execute app with a filename without an extension + return false; + } + + foreach (var extension in ExecutableExtensions) + { + if (string.Equals(fileExtension, extension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public static bool OpenCommandInShell(string? path, string? pattern, string? arguments, string? workingDir = null, ShellRunAsType runAs = ShellRunAsType.None, bool runWithHiddenWindow = false) + { + if (string.IsNullOrEmpty(pattern)) + { + // Log.Warn($"Trying to run OpenCommandInShell with an empty pattern. The default browser definition might have issues. Path: '${path ?? string.Empty}' ; Arguments: '${arguments ?? string.Empty}' ; Working Directory: '${workingDir ?? string.Empty}'", typeof(ShellHelpers)); + } + else if (pattern.Contains("%1", StringComparison.Ordinal)) + { + arguments = pattern.Replace("%1", arguments); + } + + return OpenInShell(path, arguments, workingDir, runAs, runWithHiddenWindow); + } + + 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) + { + // Log.Exception($"Unable to open {path}: {ex.Message}", ex, MethodBase.GetCurrentMethod().DeclaringType); + return false; + } + } + + public enum ShellRunAsType + { + None, + Administrator, + OtherUser, + } + + /// + /// Parses the input string to extract the executable and its arguments. + /// + public static void ParseExecutableAndArgs(string input, out string executable, out string arguments) + { + input = input.Trim(); + executable = string.Empty; + arguments = string.Empty; + + if (string.IsNullOrEmpty(input)) + { + return; + } + + if (input.StartsWith("\"", System.StringComparison.InvariantCultureIgnoreCase)) + { + // Find the closing quote + var closingQuoteIndex = input.IndexOf('\"', 1); + if (closingQuoteIndex > 0) + { + executable = input.Substring(1, closingQuoteIndex - 1); + if (closingQuoteIndex + 1 < input.Length) + { + arguments = input.Substring(closingQuoteIndex + 1).TrimStart(); + } + } + } + else + { + // Executable ends at first space + var firstSpaceIndex = input.IndexOf(' '); + if (firstSpaceIndex > 0) + { + executable = input.Substring(0, firstSpaceIndex); + arguments = input[(firstSpaceIndex + 1)..].TrimStart(); + } + else + { + executable = input; + } + } + } + + /// + /// Checks if a file exists somewhere in the PATH. + /// If it exists, returns the full path to the file in the out parameter. + /// If it does not exist, returns false and the out parameter is set to an empty string. + /// The name of the file to check. + /// The full path to the file if it exists; otherwise an empty string. + /// An optional cancellation token to cancel the operation. + /// True if the file exists in the PATH; otherwise false. + /// + public static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null) + { + fullPath = string.Empty; + + if (File.Exists(filename)) + { + token?.ThrowIfCancellationRequested(); + fullPath = Path.GetFullPath(filename); + return true; + } + else + { + var values = Environment.GetEnvironmentVariable("PATH"); + if (values is not null) + { + foreach (var path in values.Split(Path.PathSeparator)) + { + var path1 = Path.Combine(path, filename); + if (File.Exists(path1)) + { + fullPath = Path.GetFullPath(path1); + return true; + } + + token?.ThrowIfCancellationRequested(); + + var path2 = Path.Combine(path, filename + ".exe"); + if (File.Exists(path2)) + { + fullPath = Path.GetFullPath(path2); + return true; + } + + token?.ThrowIfCancellationRequested(); + } + } + + return false; + } + } + + private static bool TryResolveFromAppPaths(string name, [NotNullWhen(true)] out string? fullPath) + { + try + { + fullPath = TryHiveView(RegistryHive.CurrentUser, RegistryView.Registry64) ?? + TryHiveView(RegistryHive.CurrentUser, RegistryView.Registry32) ?? + TryHiveView(RegistryHive.LocalMachine, RegistryView.Registry64) ?? + TryHiveView(RegistryHive.LocalMachine, RegistryView.Registry32) ?? string.Empty; + + return !string.IsNullOrEmpty(fullPath); + + string? TryHiveView(RegistryHive hive, RegistryView view) + { + using var baseKey = RegistryKey.OpenBaseKey(hive, view); + using var k1 = baseKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{name}.exe"); + var val = (k1?.GetValue(null) as string)?.Trim('"'); + if (!string.IsNullOrEmpty(val)) + { + return val; + } + + // Some vendors create keys without .exe in the subkey name; check that too. + using var k2 = baseKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{name}"); + return (k2?.GetValue(null) as string)?.Trim('"'); + } + } + catch (Exception) + { + fullPath = null; + return false; + } + } + + /// + /// Mimics Windows Shell behavior to resolve an executable name to a full path. + /// + /// + /// + /// + public static bool TryResolveExecutableAsShell(string name, out string fullPath) + { + // First check if we can find the file in the registry + if (TryResolveFromAppPaths(name, out var path)) + { + fullPath = path; + return true; + } + + // If the name does not have an extension, try adding common executable extensions + // this order mimics Windows Shell behavior + // Note: HasExtension check follows Shell behavior, but differs from the + // Start Menu search results, which will offer file name with extensions + ".exe" + var nameHasExtension = Path.HasExtension(name); + if (!nameHasExtension) + { + foreach (var ext in ExecutableExtensions) + { + var nameWithExt = name + ext; + if (FileExistInPath(nameWithExt, out fullPath)) + { + return true; + } + } + } + + fullPath = string.Empty; + return false; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SmallGridLayout.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SmallGridLayout.cs new file mode 100644 index 0000000000..f8d7f63023 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/SmallGridLayout.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.CommandPalette.Extensions.Toolkit; + +public partial class SmallGridLayout : BaseObservable, ISmallGridLayout +{ +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/StatusMessage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/StatusMessage.cs new file mode 100644 index 0000000000..360cf3db5f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/StatusMessage.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class StatusMessage : BaseObservable, IStatusMessage +{ + public virtual string Message + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Message)); + } + } + += string.Empty; + + public virtual MessageState State + { + get; + set + { + field = value; + OnPropertyChanged(nameof(State)); + } + } + += MessageState.Info; + + public virtual IProgressState? Progress + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Progress)); + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Tag.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Tag.cs new file mode 100644 index 0000000000..3a2d797f55 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Tag.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class Tag : BaseObservable, ITag +{ + private OptionalColor _foreground; + private OptionalColor _background; + private string _text = string.Empty; + + public virtual OptionalColor Foreground + { + get => _foreground; + set + { + _foreground = value; + OnPropertyChanged(nameof(Foreground)); + } + } + + public virtual OptionalColor Background + { + get => _background; + set + { + _background = value; + OnPropertyChanged(nameof(Background)); + } + } + + public virtual IIconInfo Icon + { + get; + set + { + field = value; + OnPropertyChanged(nameof(Icon)); + } + } + += new IconInfo(); + + public virtual string Text + { + get => _text; + set + { + _text = value; + OnPropertyChanged(nameof(Text)); + } + } + + public virtual string ToolTip + { + get; + set + { + field = value; + OnPropertyChanged(nameof(ToolTip)); + } + } + += string.Empty; + + public Tag() + { + } + + public Tag(string text) + { + _text = text; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs new file mode 100644 index 0000000000..7cf9147159 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TextSetting.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation +// The 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; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class TextSetting : Setting +{ + public bool Multiline { get; set; } + + public string Placeholder { get; set; } = string.Empty; + + private TextSetting() + : base() + { + Value = string.Empty; + } + + public TextSetting(string key, string defaultValue) + : base(key, defaultValue) + { + } + + public TextSetting(string key, string label, string description, string defaultValue) + : base(key, label, description, defaultValue) + { + } + + public override Dictionary ToDictionary() + { + return new Dictionary + { + { "type", "Input.Text" }, + { "title", Label }, + { "id", Key }, + { "label", Description }, + { "value", Value ?? string.Empty }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + { "isMultiline", Multiline }, + { "placeholder", Placeholder }, + }; + } + + public static TextSetting LoadFromJson(JsonObject jsonObject) => new() { Value = jsonObject["value"]?.GetValue() ?? string.Empty }; + + public override void Update(JsonObject payload) + { + // If the key doesn't exist in the payload, don't do anything + if (payload[Key] is not null) + { + Value = payload[Key]?.GetValue(); + } + } + + public override string ToState() => $"\"{Key}\": {JsonSerializer.Serialize(Value, JsonSerializationContext.Default.String)}"; +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs new file mode 100644 index 0000000000..6231f3ad72 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ThumbnailHelper.cs @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation +// The 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.Drawing; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; +using Windows.Storage; +using Windows.Storage.FileProperties; +using Windows.Storage.Streams; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public static class ThumbnailHelper +{ + private static readonly string[] ImageExtensions = + [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".tiff", + ".ico", + ]; + + public static async Task GetThumbnail(string path, bool jumbo = false) + { + var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture); + var isImage = ImageExtensions.Contains(extension); + if (isImage) + { + try + { + var result = await GetImageThumbnailAsync(path, jumbo); + if (result is not null) + { + return result; + } + } + catch (Exception) + { + // ignore and fall back to icon + } + } + + try + { + return await GetFileIconStream(path, jumbo); + } + catch (Exception) + { + // ignore and return null + } + + return null; + } + + // these are windows constants and mangling them is goofy +#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 SHGFI_ICON = 0x000000100; + private const uint SHGFI_LARGEICON = 0x000000000; + private const uint SHGFI_SHELLICONSIZE = 0x000000004; + private const uint SHGFI_SYSICONINDEX = 0x000004000; + private const uint SHGFI_PIDL = 0x000000008; + private const int SHIL_JUMBO = 4; + private const int ILD_TRANSPARENT = 1; +#pragma warning restore SA1306 // Field names should begin with lower-case letter +#pragma warning restore SA1310 // Field names should not contain underscore + + // This will call DestroyIcon on the hIcon passed in. + // Duplicate it if you need it again after this. + private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon) + { + var memoryStream = new MemoryStream(); + + // Ensure disposing the icon before freeing the handle + using (var icon = Icon.FromHandle(hIcon)) + { + icon.ToBitmap().Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + } + + // Clean up the unmanaged handle without risking a use-after-free. + NativeMethods.DestroyIcon(hIcon); + + memoryStream.Position = 0; + return memoryStream; + } + + private static async Task GetFileIconStream(string filePath, bool jumbo) + { + return await TryExtractUsingPIDL(filePath, jumbo) + ?? await GetFileIconStreamUsingFilePath(filePath, jumbo); + } + + private static async Task TryExtractUsingPIDL(string shellPath, bool jumbo) + { + IntPtr pidl = 0; + try + { + var hr = NativeMethods.SHParseDisplayName(shellPath, IntPtr.Zero, out pidl, 0, out _); + if (hr != 0 || pidl == IntPtr.Zero) + { + return null; + } + + nint hIcon = 0; + if (jumbo) + { + hIcon = GetLargestIcon(pidl); + } + + if (hIcon == 0) + { + var shinfo = default(NativeMethods.SHFILEINFO); + var fileInfoResult = NativeMethods.SHGetFileInfo(pidl, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SHELLICONSIZE | SHGFI_LARGEICON | SHGFI_PIDL); + if (fileInfoResult != IntPtr.Zero && shinfo.hIcon != IntPtr.Zero) + { + hIcon = shinfo.hIcon; + } + } + + if (hIcon == 0) + { + return null; + } + + return await FromHIconToStream(hIcon); + } + catch (Exception) + { + return null; + } + finally + { + if (pidl != IntPtr.Zero) + { + NativeMethods.CoTaskMemFree(pidl); + } + } + } + + private static async Task GetFileIconStreamUsingFilePath(string filePath, bool jumbo) + { + nint hIcon = 0; + + // If requested, look up the Jumbo icon + if (jumbo) + { + hIcon = GetLargestIcon(filePath); + } + + // If we didn't want the JUMBO icon, or didn't find it, fall back to + // the normal icon lookup + if (hIcon == 0) + { + var shinfo = default(NativeMethods.SHFILEINFO); + + var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SHELLICONSIZE); + + if (hr == 0 || shinfo.hIcon == 0) + { + return null; + } + + hIcon = shinfo.hIcon; + } + + if (hIcon == 0) + { + return null; + } + + return await FromHIconToStream(hIcon); + } + + private static async Task GetImageThumbnailAsync(string filePath, bool jumbo) + { + var file = await StorageFile.GetFileFromPathAsync(filePath); + var thumbnail = await file.GetThumbnailAsync( + jumbo ? ThumbnailMode.SingleItem : ThumbnailMode.ListView, + jumbo ? 64u : 20u); + return thumbnail; + } + + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 Naming/Private")] + private static readonly Guid IID_IImageList = new Guid("46EB5926-582E-4017-9FDF-E8998DAA0950"); + + private static nint GetLargestIcon(string path) + { + var shinfo = default(NativeMethods.SHFILEINFO); + NativeMethods.SHGetFileInfo(path, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_SYSICONINDEX); + + var hIcon = IntPtr.Zero; + var iID_IImageList = IID_IImageList; + + if (NativeMethods.SHGetImageList(SHIL_JUMBO, ref iID_IImageList, out var imageListPtr) == 0 && imageListPtr != IntPtr.Zero) + { + hIcon = NativeMethods.ImageList_GetIcon(imageListPtr, shinfo.iIcon, ILD_TRANSPARENT); + } + + return hIcon; + } + + private static nint GetLargestIcon(IntPtr pidl) + { + var shinfo = default(NativeMethods.SHFILEINFO); + NativeMethods.SHGetFileInfo(pidl, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_SYSICONINDEX | SHGFI_PIDL); + + var hIcon = IntPtr.Zero; + var iID_IImageList = IID_IImageList; + + if (NativeMethods.SHGetImageList(SHIL_JUMBO, ref iID_IImageList, out var imageListPtr) == 0 && imageListPtr != IntPtr.Zero) + { + hIcon = NativeMethods.ImageList_GetIcon(imageListPtr, shinfo.iIcon, ILD_TRANSPARENT); + } + + return hIcon; + } + + /// + /// Get an icon stream for a registered URI protocol (e.g. "mailto:", "http:", "steam:"). + /// + public static async Task GetProtocolIconStream(string protocol, bool jumbo) + { + // 1) Ask the shell for the protocol's default icon "path,index" + var iconRef = QueryProtocolIconReference(protocol); + if (string.IsNullOrWhiteSpace(iconRef)) + { + return null; + } + + // Indirect reference: + if (iconRef.StartsWith('@')) + { + if (TryLoadIndirectString(iconRef, out var expanded) && !string.IsNullOrWhiteSpace(expanded)) + { + iconRef = expanded; + } + } + + // 2) Handle image files from a store app + if (File.Exists(iconRef)) + { + try + { + var file = await StorageFile.GetFileFromPathAsync(iconRef); + var thumbnail = await file.GetThumbnailAsync( + jumbo ? ThumbnailMode.SingleItem : ThumbnailMode.ListView, + jumbo ? 64u : 20u); + return thumbnail; + } + catch (Exception) + { + return null; + } + } + + // 3) Parse "path,index" (index can be negative) + if (!TryParseIconReference(iconRef, out var path, out var index)) + { + return null; + } + + // if it's and .exe and without a path, let's find on path: + if (Path.GetExtension(path).Equals(".exe", StringComparison.OrdinalIgnoreCase) && !Path.IsPathRooted(path)) + { + var paths = Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? []; + foreach (var p in paths) + { + var candidate = Path.Combine(p, path); + if (File.Exists(candidate)) + { + path = candidate; + break; + } + } + } + + // 3) Extract an HICON, preferably ~256px when jumbo==true + var hIcon = ExtractIconHandle(path, index, jumbo); + if (hIcon == 0) + { + return null; + } + + return await FromHIconToStream(hIcon); + } + + private static bool TryLoadIndirectString(string input, out string? output) + { + var outBuffer = new StringBuilder(1024); + var hr = NativeMethods.SHLoadIndirectString(input, outBuffer, outBuffer.Capacity, IntPtr.Zero); + if (hr == 0) + { + output = outBuffer.ToString(); + return !string.IsNullOrWhiteSpace(output); + } + + output = null; + return false; + } + + private static async Task FromHIconToStream(IntPtr hIcon) + { + var stream = new InMemoryRandomAccessStream(); + + using var memoryStream = GetMemoryStreamFromIcon(hIcon); // this will DestroyIcon hIcon + using var outputStream = stream.GetOutputStreamAt(0); + using var dataWriter = new DataWriter(outputStream); + + dataWriter.WriteBytes(memoryStream.ToArray()); + await dataWriter.StoreAsync(); + await dataWriter.FlushAsync(); + + return stream; + } + + private static string? QueryProtocolIconReference(string protocol) + { + // First try DefaultIcon (most widely populated for protocols) + // If you want to try AppIconReference as a fallback, you can repeat with AssocStr.AppIconReference. + var iconReference = AssocQueryStringSafe(NativeMethods.AssocStr.DefaultIcon, protocol); + if (!string.IsNullOrWhiteSpace(iconReference)) + { + return iconReference; + } + + // Optional fallback – some registrations use AppIconReference: + iconReference = AssocQueryStringSafe(NativeMethods.AssocStr.AppIconReference, protocol); + return iconReference; + + static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol) + { + uint cch = 0; + + // First call: get required length (incl. null) + _ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch); + if (cch == 0) + { + return null; + } + + // Small buffers on stack; large on heap + var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch]; + + fixed (char* p = span) + { + var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch); + if (hr != 0 || cch == 0) + { + return null; + } + + // cch includes the null terminator; slice it off + var len = (int)cch - 1; + if (len < 0) + { + len = 0; + } + + return new string(span.Slice(0, len)); + } + } + } + + private static bool TryParseIconReference(string iconRef, out string path, out int index) + { + // Typical shapes: + // "C:\Program Files\Outlook\OUTLOOK.EXE,-1" + // "shell32.dll,21" + // "\"C:\Some Path\app.dll\",-325" + + // If there's no comma, assume ",0" + index = 0; + path = iconRef.Trim(); + + // Split only on the last comma so paths with commas still work + var lastComma = path.LastIndexOf(','); + if (lastComma >= 0) + { + var idxPart = path[(lastComma + 1)..].Trim(); + path = path[..lastComma].Trim(); + _ = int.TryParse(idxPart, out index); + } + + // Trim quotes around path + path = path.Trim('"'); + if (path.Length > 1 && path[0] == '"' && path[^1] == '"') + { + path = path.Substring(1, path.Length - 2); + } + + // Basic sanity + return !string.IsNullOrWhiteSpace(path); + } + + private static nint ExtractIconHandle(string path, int index, bool jumbo) + { + // Request sizes: LOWORD=small, HIWORD=large. + // Ask for 256 when jumbo, else fall back to 32/16. + var small = jumbo ? 256 : 16; + var large = jumbo ? 256 : 32; + var sizeParam = (large << 16) | (small & 0xFFFF); + + var hr = NativeMethods.SHDefExtractIconW(path, index, 0, out var hLarge, out var hSmall, sizeParam); + if (hr == 0 && hLarge != 0) + { + return hLarge; + } + + if (hr == 0 && hSmall != 0) + { + return hSmall; + } + + // Final fallback: try 32/16 explicitly in case the resource can’t upscale + sizeParam = (32 << 16) | 16; + hr = NativeMethods.SHDefExtractIconW(path, index, 0, out hLarge, out hSmall, sizeParam); + if (hr == 0 && hLarge != 0) + { + return hLarge; + } + + if (hr == 0 && hSmall != 0) + { + return hSmall; + } + + return 0; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastArgs.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastArgs.cs new file mode 100644 index 0000000000..876ce4736f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastArgs.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.CommandPalette.Extensions.Toolkit; + +public partial class ToastArgs : IToastArgs +{ + public string? Message { get; set; } + + public ICommandResult? Result { get; set; } = CommandResult.Dismiss(); +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastStatusMessage.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastStatusMessage.cs new file mode 100644 index 0000000000..a99975af5d --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToastStatusMessage.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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ToastStatusMessage +{ + private readonly Lock _showLock = new(); + private bool _shown; + + public virtual StatusMessage Message { get; init; } + + public virtual int Duration { get; init; } = 2500; + + public ToastStatusMessage(StatusMessage message) + { + Message = message; + } + + public ToastStatusMessage(string text) + { + Message = new StatusMessage() { Message = text }; + } + + public void Show() + { + lock (_showLock) + { + if (!_shown) + { + ExtensionHost.ShowStatus(Message, StatusContext.Extension); + _ = Task.Run(() => + { + Thread.Sleep(Duration); + + lock (_showLock) + { + _shown = false; + ExtensionHost.HideStatus(Message); + } + }); + _shown = true; + } + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs new file mode 100644 index 0000000000..87beb49075 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation +// The 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; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public sealed class ToggleSetting : Setting +{ + private ToggleSetting() + : base() + { + } + + public ToggleSetting(string key, bool defaultValue) + : base(key, defaultValue) + { + } + + public ToggleSetting(string key, string label, string description, bool defaultValue) + : base(key, label, description, defaultValue) + { + } + + public override Dictionary ToDictionary() + { + var items = new List>(); + + if (!string.IsNullOrEmpty(Label)) + { + items.Add( + new() + { + { "type", "TextBlock" }, + { "text", Label }, + { "wrap", true }, + }); + } + + if (!(string.IsNullOrEmpty(Description) || string.Equals(Description, Label, StringComparison.OrdinalIgnoreCase))) + { + items.Add( + new() + { + { "type", "TextBlock" }, + { "text", Description }, + { "isSubtle", true }, + { "size", "Small" }, + { "spacing", "Small" }, + { "wrap", true }, + }); + } + + return new() + { + { "type", "ColumnSet" }, + { + "columns", new List> + { + new() + { + { "type", "Column" }, + { "width", "20px" }, + { + "items", new List> + { + new() + { + { "type", "Input.Toggle" }, + { "title", " " }, + { "id", Key }, + { "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + }, + } + }, + { "verticalContentAlignment", "Center" }, + }, + new() + { + { "type", "Column" }, + { "width", "stretch" }, + { "items", items }, + { "verticalContentAlignment", "Center" }, + }, + } + }, + { "spacing", "Medium" }, + }; + } + + public static ToggleSetting LoadFromJson(JsonObject jsonObject) => new() { Value = jsonObject["value"]?.GetValue() ?? false }; + + public override void Update(JsonObject payload) + { + // If the key doesn't exist in the payload, don't do anything + if (payload[Key] is not null) + { + // Adaptive cards returns boolean values as a string "true"/"false", cause of course. + var strFromJson = payload[Key]?.GetValue() ?? string.Empty; + var val = strFromJson switch { "true" => true, "false" => false, _ => false }; + Value = val; + } + } + + public override string ToState() + { + var adaptiveCardsUsesStringsForBools = Value ? "true" : "false"; + return $"\"{Key}\": \"{adaptiveCardsUsesStringsForBools}\""; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TreeContent.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TreeContent.cs new file mode 100644 index 0000000000..dfb2b9b447 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/TreeContent.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 Windows.Foundation; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class TreeContent : BaseObservable, ITreeContent +{ + public IContent[] Children { get; set; } = []; + + public virtual IContent? RootContent + { + get; + set + { + field = value; + OnPropertyChanged(nameof(RootContent)); + } + } + + public event TypedEventHandler? ItemsChanged; + + public virtual IContent[] GetChildren() => Children; + + protected void RaiseItemsChanged(int totalItems = -1) + { + try + { + // TODO #181 - This is the same thing that BaseObservable has to deal with. + ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems)); + } + catch + { + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Utilities.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Utilities.cs new file mode 100644 index 0000000000..50e56bdefb --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Utilities.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.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "This file has more than a couple Windows constants in it, which don't make sense to rename")] +public class Utilities +{ + /// + /// Used to produce a path to a settings folder which your app can use. + /// If your app is running packaged, this will return the redirected local + /// app data path (Packages/{your_pfn}/LocalState). If not, it'll return + /// %LOCALAPPDATA%\{settingsFolderName}. + /// + /// Does not ensure that the directory exists. Callers should call + /// CreateDirectory before writing settings files to this directory. + /// + /// + /// var directory = Utilities.BaseSettingsPath("Some.Unique.String.Here"); + /// Directory.CreateDirectory(directory); + /// + /// A fallback directory name to use + /// inside of %LocalAppData%, in the case this app is not currently running + /// in a package context + /// The path to a folder to use for storing settings. + public static string BaseSettingsPath(string settingsFolderName) + { + // KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return + // the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests. + // Using this flag allows us to avoid Windows.Storage.ApplicationData completely. + var FOLDERID_LocalAppData = new Guid("F1B32785-6FBA-4FCF-9D55-7B8E7F157091"); + var hr = PInvoke.SHGetKnownFolderPath( + FOLDERID_LocalAppData, + KNOWN_FOLDER_FLAG.KF_FLAG_FORCE_APP_DATA_REDIRECTION, + null, + out var localAppDataFolder); + + if (hr.Succeeded) + { + var basePath = new string(localAppDataFolder.ToString()); + if (!IsPackaged()) + { + basePath = Path.Combine(basePath, settingsFolderName); + } + + return basePath; + } + else + { + throw Marshal.GetExceptionForHR(hr.Value)!; + } + } + + /// + /// Can be used to quickly determine if this process is running with package identity. + /// + /// true iff the process is running with package identity + public static bool IsPackaged() + { + uint bufferSize = 0; + var bytes = Array.Empty(); + + // CsWinRT apparently won't generate this constant + var APPMODEL_ERROR_NO_PACKAGE = (WIN32_ERROR)15700; + unsafe + { + fixed (byte* p = bytes) + { + // We don't actually need the package ID. We just need to know + // if we have a package or not, and APPMODEL_ERROR_NO_PACKAGE + // is a quick way to find out. + var win32Error = PInvoke.GetCurrentPackageId(ref bufferSize, p); + return win32Error != APPMODEL_ERROR_NO_PACKAGE; + } + } + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/WeakEventListener`3.cs b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/WeakEventListener`3.cs new file mode 100644 index 0000000000..cd3a62a079 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/WeakEventListener`3.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.ComponentModel; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +/// +/// Implements a weak event listener that allows the owner to be garbage +/// collected if its only remaining link is an event handler. +/// +/// Type of instance listening for the event. +/// Type of source for the event. +/// Type of event arguments for the event. +[EditorBrowsable(EditorBrowsableState.Never)] +internal sealed class WeakEventListener + where TInstance : class +{ + /// + /// WeakReference to the instance listening for the event. + /// + private readonly WeakReference _weakInstance; + + /// + /// Initializes a new instance of the class. + /// + /// Instance subscribing to the event. + /// Event handler executed when event is raised. + /// Action to execute when instance was collected. + public WeakEventListener( + TInstance instance, + Action? onEventAction = null, + Action>? onDetachAction = null) + { + ArgumentNullException.ThrowIfNull(instance); + + _weakInstance = new(instance); + OnEventAction = onEventAction; + OnDetachAction = onDetachAction; + } + + /// + /// Gets or sets the method to call when the event fires. + /// + public Action? OnEventAction { get; set; } + + /// + /// Gets or sets the method to call when detaching from the event. + /// + public Action>? OnDetachAction { get; set; } + + /// + /// Handler for the subscribed event calls OnEventAction to handle it. + /// + /// Event source. + /// Event arguments. + public void OnEvent(TSource source, TEventArgs eventArgs) + { + if (_weakInstance.TryGetTarget(out var target)) + { + // Call registered action + OnEventAction?.Invoke(target, source, eventArgs); + } + else + { + // Detach from event + Detach(); + } + } + + /// + /// Detaches from the subscribed event. + /// + public void Detach() + { + OnDetachAction?.Invoke(this); + OnDetachAction = null; + } +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.def b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.def new file mode 100644 index 0000000000..b95ee2cd0f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl new file mode 100644 index 0000000000..68fd928955 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl @@ -0,0 +1,396 @@ +namespace Microsoft.CommandPalette.Extensions +{ + [contractversion(1)] + apicontract ExtensionsContract {} + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IExtension { + IInspectable GetProvider(ProviderType providerType); + void Dispose(); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + enum ProviderType { + Commands = 0, + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IIconData { + String Icon { get; }; + Windows.Storage.Streams.IRandomAccessStreamReference Data { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IIconInfo { + IIconData Light { get; }; + IIconData Dark { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + struct KeyChord + { + Windows.System.VirtualKeyModifiers Modifiers; + Int32 Vkey; + Int32 ScanCode; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface INotifyPropChanged { + event Windows.Foundation.TypedEventHandler PropChanged; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IPropChangedEventArgs { + String PropertyName { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface INotifyItemsChanged { + event Windows.Foundation.TypedEventHandler ItemsChanged; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IItemsChangedEventArgs { + Int32 TotalItems { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommand requires INotifyPropChanged{ + String Name{ get; }; + String Id{ get; }; + IIconInfo Icon{ get; }; + } + + enum CommandResultKind { + Dismiss, // Reset the palette to the main page and dismiss + GoHome, // Go back to the main page, but keep it open + GoBack, // Go back one level + Hide, // Keep this page open, but hide the palette. + KeepOpen, // Do nothing. + GoToPage, // Go to another page. GoToPageArgs will tell you where. + ShowToast, // Display a transient message to the user + Confirm, // Display a confirmation dialog + }; + + enum NavigationMode { + Push, // Push the target page onto the navigation stack + GoBack, // Go back one page before navigating to the target page + GoHome, // Go back to the home page before navigating to the target page + }; + + [uuid("f9d6423b-bd5e-44bb-a204-2f5c77a72396")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandResultArgs{}; + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandResult { + CommandResultKind Kind { get; }; + ICommandResultArgs Args { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IGoToPageArgs requires ICommandResultArgs{ + String PageId { get; }; + NavigationMode NavigationMode { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IToastArgs requires ICommandResultArgs{ + String Message { get; }; + ICommandResult Result { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IConfirmationArgs requires ICommandResultArgs{ + String Title { get; }; + String Description { get; }; + ICommand PrimaryCommand { get; }; + Boolean IsPrimaryCommandCritical { get; }; + } + + // This is a "leaf" of the UI. This is something that can be "done" by the user. + // * A ListPage + // * the MoreCommands flyout of for a ListItem or a MarkdownPage + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IInvokableCommand requires ICommand { + ICommandResult Invoke(Object sender); + } + + + [uuid("ef5db50c-d26b-4aee-9343-9f98739ab411")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFilterItem {} + + [uuid("0a923c7f-5b7b-431d-9898-3c8c841d02ed")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ISeparatorFilterItem requires IFilterItem {} + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFilter requires INotifyPropChanged, IFilterItem { + String Id { get; }; + String Name { get; }; + IIconInfo Icon { get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFilters { + String CurrentFilterId { get; set; }; + IFilterItem[] GetFilters(); + } + + struct Color + { + UInt8 R; + UInt8 G; + UInt8 B; + UInt8 A; + }; + + struct OptionalColor + { + Boolean HasValue; + Microsoft.CommandPalette.Extensions.Color Color; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ITag { + IIconInfo Icon { get; }; + String Text { get; }; + OptionalColor Foreground { get; }; + OptionalColor Background { get; }; + String ToolTip { get; }; + }; + + [uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsData {} + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsElement { + String Key { get; }; + IDetailsData Data { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetails { + IIconInfo HeroImage { get; }; + String Title { get; }; + String Body { get; }; + IDetailsElement[] Metadata { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsTags requires IDetailsData { + ITag[] Tags { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsLink requires IDetailsData { + Windows.Foundation.Uri Link { get; }; + String Text { get; }; + } + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsCommands requires IDetailsData { + ICommand[] Commands { get; }; + } + [uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDetailsSeparator requires IDetailsData {} + + enum MessageState + { + Info = 0, + Success, + Warning, + Error, + }; + + enum StatusContext + { + Page, + Extension + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IProgressState requires INotifyPropChanged + { + Boolean IsIndeterminate { get; }; + UInt32 ProgressPercent { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IStatusMessage requires INotifyPropChanged + { + MessageState State { get; }; + IProgressState Progress { get; }; + String Message { get; }; + // TODO! Icon maybe? Work with design on this + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ILogMessage + { + MessageState State { get; }; + String Message { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IExtensionHost + { + Windows.Foundation.IAsyncAction ShowStatus(IStatusMessage message, StatusContext context); + Windows.Foundation.IAsyncAction HideStatus(IStatusMessage message); + + Windows.Foundation.IAsyncAction LogMessage(ILogMessage message); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IPage requires ICommand { + String Title { get; }; + Boolean IsLoading { get; }; + + OptionalColor AccentColor { get; }; + } + + [uuid("c78b9851-e76b-43ee-8f76-da5ba14e69a4")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IContextItem {} + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandItem requires INotifyPropChanged { + ICommand Command{ get; }; + IContextItem[] MoreCommands{ get; }; + IIconInfo Icon{ get; }; + String Title{ get; }; + String Subtitle{ get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandContextItem requires ICommandItem, IContextItem { + Boolean IsCritical { get; }; // READ: "make this red" + KeyChord RequestedShortcut { get; }; + } + + [uuid("924a87fc-32fe-4471-9156-84b3b30275a6")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ISeparatorContextItem requires IContextItem {} + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IListItem requires ICommandItem { + ITag[] Tags{ get; }; + IDetails Details{ get; }; + String Section { get; }; + String TextToSuggest { get; }; + } + + [uuid("50C6F080-1CBE-4CE4-B92F-DA2F116ED524")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IGridProperties requires INotifyPropChanged { } + + [uuid("05914D59-6ECB-4992-9CF2-5982B5120A26")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ISmallGridLayout requires IGridProperties { } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IMediumGridLayout requires IGridProperties + { + Boolean ShowTitle { get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IGalleryGridLayout requires IGridProperties + { + Boolean ShowTitle { get; }; + Boolean ShowSubtitle { get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IListPage requires IPage, INotifyItemsChanged { + // DevPal will be responsible for filtering the list of items, unless the + // class implements IDynamicListPage + String SearchText { get; }; + String PlaceholderText { get; }; + Boolean ShowDetails{ get; }; + IFilters Filters { get; }; + IGridProperties GridProperties { get; }; + Boolean HasMoreItems { get; }; + ICommandItem EmptyContent { get; }; + + IListItem[] GetItems(); + void LoadMore(); + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IDynamicListPage requires IListPage { + String SearchText { set; }; + } + + [uuid("b64def0f-8911-4afa-8f8f-042bd778d088")] + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IContent requires INotifyPropChanged { + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFormContent requires IContent { + String TemplateJson { get; }; + String DataJson { get; }; + String StateJson { get; }; + ICommandResult SubmitForm(String inputs, String data); + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IMarkdownContent requires IContent { + String Body { get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ITreeContent requires IContent, INotifyItemsChanged { + IContent RootContent { get; }; + IContent[] GetChildren(); + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IContentPage requires IPage, INotifyItemsChanged { + IContent[] GetContent(); + IDetails Details { get; }; + IContextItem[] Commands { get; }; + } + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandSettings { + IContentPage SettingsPage { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFallbackHandler { + void UpdateQuery(String query); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IFallbackCommandItem requires ICommandItem { + IFallbackHandler FallbackHandler{ get; }; + String DisplayTitle { get; }; + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandProvider requires Windows.Foundation.IClosable, INotifyItemsChanged + { + String Id { get; }; + String DisplayName { get; }; + IIconInfo Icon { get; }; + ICommandSettings Settings { get; }; + Boolean Frozen { get; }; + + ICommandItem[] TopLevelCommands(); + IFallbackCommandItem[] FallbackCommands(); + + ICommand GetCommand(String id); + + void InitializeWithHost(IExtensionHost host); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IExtendedAttributesProvider + { + Windows.Foundation.Collections.IMap GetProperties(); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandProvider2 requires ICommandProvider + { + Object[] GetApiExtensionStubs(); + }; + + +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj new file mode 100644 index 0000000000..3d834c4fda --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj @@ -0,0 +1,183 @@ + + + + ..\..\..\..\..\ + $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.8.250907003 + $(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5 + $(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188 + $(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40 + + + + + + true + true + true + true + {7997DAD4-31D6-496B-95DB-6C028D699370} + Microsoft.CommandPalette.Extensions + Microsoft.CommandPalette.Extensions + en-US + 14.0 + false + Windows Store + 10.0 + 10.0.19041.0 + 10.0.26100.0 + + + + + Debug + ARM64 + + + Debug + x64 + + + Release + ARM64 + + + Release + x64 + + + + $(SolutionDir)$(Platform)\$(Configuration)\Microsoft.CommandPalette.Extensions\ + obj\$(Platform)\$(Configuration)\ + + + DynamicLibrary + v143 + v142 + v141 + v140 + Unicode + false + true + + + true + true + + + false + true + false + + + + + MultiThreadedDebug + + + + %(IgnoreSpecificDefaultLibraries);libucrtd.lib + %(AdditionalOptions) /defaultlib:ucrtd.lib /profile /opt:ref /opt:icf + + + + + + MultiThreaded + + + + %(IgnoreSpecificDefaultLibraries);libucrt.lib + %(AdditionalOptions) /defaultlib:ucrt.lib /profile /opt:ref /opt:icf + + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + stdcpp20 + %(AdditionalOptions) /bigobj /Zi + _WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + Guard + Spectre + + + Console + false + Microsoft.CommandPalette.Extensions.def + + + + + _DEBUG;%(PreprocessorDefinitions) + ProgramDatabase + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + Create + + + + + + + + + + + + false + + + + + + + + + + + + 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/Deux/SDK/Microsoft.CommandPalette.Extensions/packages.config b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/packages.config new file mode 100644 index 0000000000..e945c5824d --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/packages.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.cpp b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.h b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.h new file mode 100644 index 0000000000..b3aec123db --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/pch.h @@ -0,0 +1,4 @@ +#pragma once +#include +#include +#include \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/readme.txt b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/readme.txt new file mode 100644 index 0000000000..49d8e990b3 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/readme.txt @@ -0,0 +1,2 @@ +The DLL that this project builds exists only to satisfy the build system, and is otherwise unused. +This Project provides winmd to the SDK project for building projections. \ No newline at end of file diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/til/winrt.h b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/til/winrt.h new file mode 100644 index 0000000000..984066e3b8 --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/til/winrt.h @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + template + struct property + { + explicit constexpr property(auto&&... args) : + _value{ std::forward(args)... } {} + + property& operator=(const property& other) = default; + + T operator()() const noexcept + { + return _value; + } + void operator()(auto&& arg) + { + _value = std::forward(arg); + } + explicit operator bool() const noexcept + { +#ifdef WINRT_Windows_Foundation_H + if constexpr (std::is_same_v) + { + return !_value.empty(); + } + else +#endif + { + return _value; + } + } + bool operator==(const property& other) const noexcept + { + return _value == other._value; + } + bool operator!=(const property& other) const noexcept + { + return _value != other._value; + } + bool operator==(const T& other) const noexcept + { + return _value == other; + } + bool operator!=(const T& other) const noexcept + { + return _value != other; + } + + private: + T _value; + }; + +#ifdef WINRT_Windows_Foundation_H + + template + struct event + { + event() = default; + winrt::event_token operator()(const ArgsT& handler) { return _handlers.add(handler); } + void operator()(const winrt::event_token& token) { _handlers.remove(token); } + operator bool() const noexcept { return bool(_handlers); } + template + void raise(auto&&... args) + { + _handlers(std::forward(args)...); + } + winrt::event _handlers; + }; + + template + struct typed_event + { + typed_event() = default; + winrt::event_token operator()(const winrt::Windows::Foundation::TypedEventHandler& handler) { return _handlers.add(handler); } + void operator()(const winrt::event_token& token) { _handlers.remove(token); } + operator bool() const noexcept { return bool(_handlers); } + template + void raise(Arg const&... args) + { + _handlers(std::forward(args)...); + } + winrt::event> _handlers; + }; +#endif +#ifdef WINRT_Windows_UI_Xaml_Data_H + + using property_changed_event = til::event; + // Making a til::observable_property unfortunately doesn't seem feasible. + // It's gonna just result in more macros, which no one wants. + // + // 1. We don't know who the sender is, or would require `this` to always be + // the first parameter to one of these observable_property's. + // + // 2. We don't know what our own name is. We need to actually raise an event + // with the name of the variable as the parameter. Only way to do that is + // with something like + // + // til::observable Foo(this, L"Foo", 42) + // + // which then implies the creation of: + // + // #define OBSERVABLE(type, name, ...) til::observable_property name{ this, L## #name, this.PropertyChanged, __VA_ARGS__ }; + // + // Which is just silly + +#endif +} diff --git a/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/winrt_module.cpp b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/winrt_module.cpp new file mode 100644 index 0000000000..17a5c3967f --- /dev/null +++ b/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/winrt_module.cpp @@ -0,0 +1,58 @@ +// TYPICALLY: This file is generated by C++/WinRT (this one was originally from +// v2.0.240111.5), and it's included in the dll by way of "Generated +// Files\module.g.cpp". However, our project doesn't have any activatable WinRT +// `runtimeclass`es in it. But we need something to appease WinRT, so this is +// the empty version of this file, without any classes in it. +// +// Should you need to add a WinRT runtime class to this project, think very +// carefully about it first. If that's the only solution, then just delete this +// file. + +#include "pch.h" +#include "winrt/base.h" + +bool __stdcall winrt_can_unload_now() noexcept +{ + if (winrt::get_module_lock()) + { + return false; + } + + winrt::clear_factory_cache(); + return true; +} + +void* __stdcall winrt_get_activation_factory([[maybe_unused]] std::wstring_view const& name) +{ + return nullptr; +} + +int32_t __stdcall WINRT_CanUnloadNow() noexcept +{ +#ifdef _WRL_MODULE_H_ + if (!::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().Terminate()) + { + return 1; + } +#endif + + return winrt_can_unload_now() ? 0 : 1; +} + +int32_t __stdcall WINRT_GetActivationFactory(void* classId, void** factory) noexcept try +{ + std::wstring_view const name{ *reinterpret_cast(&classId) }; + *factory = winrt_get_activation_factory(name); + + if (*factory) + { + return 0; + } + +#ifdef _WRL_MODULE_H_ + return ::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().GetActivationFactory(static_cast(classId), reinterpret_cast<::IActivationFactory**>(factory)); +#else + return winrt::hresult_class_not_available(name).to_abi(); +#endif +} +catch (...) { return winrt::to_hresult(); } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.cs new file mode 100644 index 0000000000..b0ece6267b --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/AppStateModel.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. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CommandPalette.UI.Models; +using Microsoft.Extensions.Logging; +using Windows.Foundation; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class AppStateModel : ObservableObject +{ + private static string _filePath; + + public event TypedEventHandler? StateChanged; + + /////////////////////////////////////////////////////////////////////////// + // STATE HERE + // Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)! + // Make sure that any new types you add are added to JsonSerializationContext! + public List RunHistory { get; set; } = []; + + // END STATE + /////////////////////////////////////////////////////////////////////////// + + static AppStateModel() + { + _filePath = PersistenceService.SettingsJsonPath("state.json"); + } + + public static AppStateModel LoadState(ILogger logger) + { + return PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.AppStateModel!, logger); + } + + public static void SaveState(AppStateModel model, ILogger logger) + { + try + { + PersistenceService.SaveObject( + model, + _filePath, + JsonSerializationContext.Default.AppStateModel!, + JsonSerializationContext.Default.AppStateModel!.Options, + beforeWriteMutation: null, + afterWriteCallback: m => m.StateChanged?.Invoke(m, null), + logger); + } + catch (Exception ex) + { + Log_SaveStateFailure(logger, _filePath, ex); + } + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to save application state to '{filePath}'.")] + static partial void Log_SaveStateFailure(ILogger logger, string filePath, Exception exception); +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/LayoutMapHelper.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/LayoutMapHelper.cs new file mode 100644 index 0000000000..7ff3e7bc5b --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Helpers/LayoutMapHelper.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.CommandPalette.UI.Models.Helpers; + +public static class LayoutMapHelper +{ + 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/Deux/UI/Microsoft.CommandPalette.UI.Models/HotkeySettings.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/HotkeySettings.cs new file mode 100644 index 0000000000..6b0085340b --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/HotkeySettings.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation +// The 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; +using Microsoft.CommandPalette.UI.Models.Helpers; + +namespace Microsoft.CommandPalette.UI.Models; + +public record HotkeySettings +{ + 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 = LayoutMapHelper.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 = LayoutMapHelper.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)LayoutMapHelper.GetKeyValue(key); + return keyCode != 0; + } + } + + public bool TryToCmdRepresentable(out string result) + { + result = ToString(); + result = result.Replace(" ", null); + return true; + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.cs new file mode 100644 index 0000000000..0d5a2aa646 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/JsonSerializationContext.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 System.Text.Json.Serialization; +using Microsoft.CmdPal.UI.ViewModels; + +namespace Microsoft.CommandPalette.UI.Models; + +[JsonSerializable(typeof(float))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(SettingsModel))] +[JsonSerializable(typeof(WindowPosition))] +[JsonSerializable(typeof(AppStateModel))] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "StringList")] +[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")] +[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")] +internal sealed partial class JsonSerializationContext : JsonSerializerContext +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/HotkeySummonMessage.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/HotkeySummonMessage.cs new file mode 100644 index 0000000000..798a6a562b --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/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.Models.Messages; + +public record HotkeySummonMessage(string CommandId, IntPtr Hwnd) +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/OpenSettingsMessage.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/OpenSettingsMessage.cs new file mode 100644 index 0000000000..a1c75af570 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/OpenSettingsMessage.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.Models.Messages; + +public record OpenSettingsMessage() +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/QuitMessage.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/QuitMessage.cs new file mode 100644 index 0000000000..8456e0dbdf --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Messages/QuitMessage.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.Models.Messages; + +/// +/// Message which closes the application. Used by via . +/// +public record QuitMessage() +{ +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Microsoft.CommandPalette.UI.Models.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Microsoft.CommandPalette.UI.Models.csproj new file mode 100644 index 0000000000..3ea1e96d99 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Microsoft.CommandPalette.UI.Models.csproj @@ -0,0 +1,46 @@ + + + + + + enable + enable + false + false + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CommandPalette + + preview + + SA1313; + + + + AdaptiveCards.ObjectModel.WinUI3;AdaptiveCards.Rendering.WinUI3 + true + + + + + + + + + + + + + + + runtimes\win10-$(Platform)\native + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/MonitorBehavior.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/MonitorBehavior.cs new file mode 100644 index 0000000000..b1710bad7b --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/MonitorBehavior.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.CommandPalette.UI.Models; + +public enum MonitorBehavior +{ + ToMouse = 0, + ToPrimary = 1, + ToFocusedWindow = 2, + InPlace = 3, + ToLast = 4, +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs new file mode 100644 index 0000000000..083f51045d --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Services/PersistenceService.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation +// The 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; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CommandPalette.UI.Models; + +public partial class PersistenceService +{ + private static bool TryParseJsonObject(string json, ILogger logger, [NotNullWhen(true)] out JsonObject? obj) + { + obj = null; + try + { + obj = JsonNode.Parse(json) as JsonObject; + return obj is not null; + } + catch (Exception ex) + { + Log_PersistenceParseFailure(logger, ex); + return false; + } + } + + private static bool TryReadSavedObject(string filePath, ILogger logger, [NotNullWhen(true)] out JsonObject? saved) + { + saved = null; + + string oldContent; + try + { + if (!File.Exists(filePath)) + { + saved = new JsonObject(); + return true; + } + + oldContent = File.ReadAllText(filePath); + } + catch (Exception ex) + { + Log_PersistenceReadFileFailure(logger, filePath, ex); + return false; + } + + if (string.IsNullOrWhiteSpace(oldContent)) + { + Log_FileEmpty(logger, filePath); + return false; + } + + return TryParseJsonObject(oldContent, logger, out saved); + } + + public static T LoadObject(string filePath, JsonTypeInfo typeInfo, ILogger logger) + where T : new() + { + if (string.IsNullOrEmpty(filePath)) + { + throw new InvalidOperationException($"You must set a valid file path before loading {typeof(T).Name}"); + } + + if (!File.Exists(filePath)) + { + Log_FileDoesntExist(logger, typeof(T).Name, filePath); + return new T(); + } + + try + { + var jsonContent = File.ReadAllText(filePath); + var loaded = JsonSerializer.Deserialize(jsonContent, typeInfo); + return loaded ?? new T(); + } + catch (Exception ex) + { + Log_PersistenceReadFailure(logger, typeof(T).Name, filePath, ex); + return new T(); + } + } + + public static void SaveObject( + T model, + string filePath, + JsonTypeInfo typeInfo, + JsonSerializerOptions optionsForWrite, + Action? beforeWriteMutation, + Action? afterWriteCallback, + ILogger logger) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new InvalidOperationException($"You must set a valid file path before saving {typeof(T).Name}"); + } + + try + { + var json = JsonSerializer.Serialize(model, typeInfo); + + if (!TryParseJsonObject(json, logger, out var newObj)) + { + Log_SerializationError(logger, typeof(T).Name); + return; + } + + if (!TryReadSavedObject(filePath, logger, out var savedObj)) + { + savedObj = new JsonObject(); + } + + foreach (var kvp in newObj) + { + savedObj[kvp.Key] = kvp.Value?.DeepClone(); + } + + beforeWriteMutation?.Invoke(savedObj); + + var serialized = savedObj.ToJsonString(optionsForWrite); + File.WriteAllText(filePath, serialized); + + afterWriteCallback?.Invoke(model); + } + catch (Exception ex) + { + Log_PersistenceSaveFailure(logger, typeof(T).Name, filePath, ex); + } + } + + internal static string SettingsJsonPath(string fileName) + { + var directory = Utilities.BaseSettingsPath("Microsoft.CommandPalette"); + Directory.CreateDirectory(directory); + + // now, the settings is just next to the exe + return Path.Combine(directory, fileName); + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to save {typeName} to '{filePath}'.")] + static partial void Log_PersistenceSaveFailure(ILogger logger, string typeName, string filePath, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to read {typeName} from '{filePath}'.")] + static partial void Log_PersistenceReadFailure(ILogger logger, string typeName, string filePath, Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to serialize {typeName} to JsonObject.")] + static partial void Log_SerializationError(ILogger logger, string typeName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "The provided {typeName} file does not exist ({filePath})")] + static partial void Log_FileDoesntExist(ILogger logger, string typeName, string filePath); + + [LoggerMessage(Level = LogLevel.Debug, Message = "The file at '{filePath}' is empty.")] + static partial void Log_FileEmpty(ILogger logger, string filePath); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to read file at '{filePath}'.")] + static partial void Log_PersistenceReadFileFailure(ILogger logger, string filePath, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to parse persisted JSON.")] + static partial void Log_PersistenceParseFailure(ILogger logger, Exception exception); +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs new file mode 100644 index 0000000000..1ccbe05f37 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/SettingsModel.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation +// The 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 System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; +using Windows.Foundation; + +namespace Microsoft.CommandPalette.UI.Models; + +public partial class SettingsModel : ObservableObject +{ + private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome"; + + [JsonIgnore] + private static readonly string _filePath; + + public event TypedEventHandler? SettingsChanged; + + /////////////////////////////////////////////////////////////////////////// + // SETTINGS HERE + public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space + + public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut; + + public bool UseLowLevelGlobalHotkey { get; set; } + + public bool ShowAppDetails { get; set; } + + public bool BackspaceGoesBack { get; set; } + + public bool SingleClickActivates { get; set; } + + public bool HighlightSearchOnActivate { get; set; } = true; + + public bool ShowSystemTrayIcon { get; set; } = true; + + public bool IgnoreShortcutWhenFullscreen { get; set; } + + public bool AllowExternalReload { get; set; } + + public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse; + + public bool DisableAnimations { get; set; } = true; + + public bool EnableAnimations { get; set; } + + public WindowPosition? LastWindowPosition { get; set; } + + public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan; + + // END SETTINGS + /////////////////////////////////////////////////////////////////////////// + + static SettingsModel() + { + _filePath = PersistenceService.SettingsJsonPath("settings.json"); + } + + private static bool ApplyMigrations(JsonObject root, SettingsModel model, ILogger logger) + { + var migrated = false; + + migrated |= TryMigrate( + "Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)", + root, + model, + nameof(AutoGoHomeInterval), + DeprecatedHotkeyGoesHomeKey, + (settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan, + JsonSerializationContext.Default.Boolean, + logger); + + return migrated; + } + + private static bool TryMigrate(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action apply, JsonTypeInfo jsonTypeInfo, ILogger logger) + { + try + { + if (root.ContainsKey(newKey) && root[newKey] is not null) + { + return false; + } + + if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null) + { + var value = oldNode.Deserialize(jsonTypeInfo); + apply(model, value!); + return true; + } + } + catch (Exception ex) + { + Log_MigrationFailure(logger, migrationName, ex); + } + + return false; + } + + public static SettingsModel LoadSettings(ILogger logger) + { + var settings = PersistenceService.LoadObject(_filePath, JsonSerializationContext.Default.SettingsModel!, logger); + + var migratedAny = false; + try + { + var jsonContent = File.Exists(_filePath) ? File.ReadAllText(_filePath) : "{}"; + if (JsonNode.Parse(jsonContent) is JsonObject root) + { + migratedAny |= ApplyMigrations(root, settings, logger); + } + } + catch (Exception ex) + { + Log_MigrationCheckFailure(logger, ex); + } + + if (migratedAny) + { + SaveSettings(settings, logger); + } + + return settings; + } + + public static void SaveSettings(SettingsModel model, ILogger logger) + { + PersistenceService.SaveObject( + model, + _filePath, + JsonSerializationContext.Default.SettingsModel, + JsonSerializationContext.Default.Options, + beforeWriteMutation: obj => obj.Remove(DeprecatedHotkeyGoesHomeKey), + afterWriteCallback: m => m.SettingsChanged?.Invoke(m, null), + logger); + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration '{MigrationName}' failed.")] + static partial void Log_MigrationFailure(ILogger logger, string MigrationName, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Settings migration check failed.")] + static partial void Log_MigrationCheckFailure(ILogger logger, Exception exception); +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/WindowPosition.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/WindowPosition.cs new file mode 100644 index 0000000000..b2f6455a2e --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/WindowPosition.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 Windows.Graphics; + +namespace Microsoft.CommandPalette.UI.Models; + +public sealed class WindowPosition +{ + /// + /// Gets or sets left position in device pixels. + /// + public int X { get; set; } + + /// + /// Gets or sets top position in device pixels. + /// + public int Y { get; set; } + + /// + /// Gets or sets width in device pixels. + /// + public int Width { get; set; } + + /// + /// Gets or sets height in device pixels. + /// + public int Height { get; set; } + + /// + /// Gets or sets width of the screen in device pixels where the window is located. + /// + public int ScreenWidth { get; set; } + + /// + /// Gets or sets height of the screen in device pixels where the window is located. + /// + public int ScreenHeight { get; set; } + + /// + /// Gets or sets DPI (dots per inch) of the display where the window is located. + /// + public int Dpi { get; set; } + + /// + /// Converts the window position properties to a structure representing the physical window rectangle. + /// + public RectInt32 ToPhysicalWindowRectangle() + { + return new RectInt32(X, Y, Width, Height); + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj new file mode 100644 index 0000000000..5580976379 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj @@ -0,0 +1,29 @@ + + + + + + enable + enable + false + false + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CommandPalette + + preview + + SA1313; + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000000..e3adadbd1c --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/SettingsViewModel.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.ComponentModel; +using Microsoft.CommandPalette.UI.Models; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CommandPalette.UI.ViewModels.Settings; + +public partial class SettingsViewModel : INotifyPropertyChanged +{ + private static readonly List AutoGoHomeIntervals = + [ + Timeout.InfiniteTimeSpan, + TimeSpan.Zero, + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(20), + TimeSpan.FromSeconds(30), + TimeSpan.FromSeconds(60), + TimeSpan.FromSeconds(90), + TimeSpan.FromSeconds(120), + TimeSpan.FromSeconds(180), + ]; + + private readonly ILogger logger; + private readonly SettingsModel _settings; + private readonly IServiceProvider _serviceProvider; + + public event PropertyChangedEventHandler? PropertyChanged; + + public HotkeySettings? Hotkey + { + get => _settings.Hotkey; + set + { + _settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey))); + Save(); + } + } + + public bool UseLowLevelGlobalHotkey + { + get => _settings.UseLowLevelGlobalHotkey; + set + { + _settings.UseLowLevelGlobalHotkey = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey))); + Save(); + } + } + + public bool AllowExternalReload + { + get => _settings.AllowExternalReload; + set + { + _settings.AllowExternalReload = value; + Save(); + } + } + + public bool ShowAppDetails + { + get => _settings.ShowAppDetails; + set + { + _settings.ShowAppDetails = 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 bool ShowSystemTrayIcon + { + get => _settings.ShowSystemTrayIcon; + set + { + _settings.ShowSystemTrayIcon = value; + Save(); + } + } + + public bool IgnoreShortcutWhenFullscreen + { + get => _settings.IgnoreShortcutWhenFullscreen; + set + { + _settings.IgnoreShortcutWhenFullscreen = value; + Save(); + } + } + + public bool DisableAnimations + { + get => _settings.DisableAnimations; + set + { + _settings.DisableAnimations = value; + Save(); + } + } + + public int AutoGoBackIntervalIndex + { + get + { + var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval); + return index >= 0 ? index : 0; + } + + set + { + if (value >= 0 && value < AutoGoHomeIntervals.Count) + { + _settings.AutoGoHomeInterval = AutoGoHomeIntervals[value]; + } + + Save(); + } + } + + // public ObservableCollection CommandProviders { get; } = []; + // public SettingsExtensionsViewModel Extensions { get; } + public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler, ILogger logger) + { + _settings = settings; + _serviceProvider = serviceProvider; + this.logger = logger; + + // var activeProviders = GetCommandProviders(); + // var allProviderSettings = _settings.ProviderSettings; + // foreach (var item in activeProviders) + // { + // var providerSettings = settings.GetProviderSettings(item); + // var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider); + // CommandProviders.Add(settingsModel); + // } + // Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler); + } + + // private IEnumerable GetCommandProviders() + // { + // var manager = _serviceProvider.GetService()!; + // var allProviders = manager.CommandProviders; + // return allProviders; + // } + private void Save() => SettingsModel.SaveSettings(_settings, logger); +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/ShellViewModel.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/ShellViewModel.cs new file mode 100644 index 0000000000..01350fa735 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/ShellViewModel.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 CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CommandPalette.ViewModels; + +public partial class ShellViewModel : ObservableObject +{ + private readonly ILogger logger; + private readonly TaskScheduler _scheduler; + + [ObservableProperty] + public partial bool IsLoaded { get; set; } = false; + + private bool _isNested; + + public bool IsNested => _isNested; + + public ShellViewModel( + TaskScheduler scheduler, + ILogger logger) + { + this.logger = logger; + _scheduler = scheduler; + _isNested = false; + } + + public void GoHome(bool withAnimation = true, bool focusSearch = true) + { + } + + public void GoBack(bool withAnimation = true, bool focusSearch = true) + { + } + + private void OnUIThread(Action action) + { + _ = Task.Factory.StartNew( + action, + CancellationToken.None, + TaskCreationOptions.None, + _scheduler); + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml index 61e73ce648..173c29edf1 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml @@ -8,9 +8,12 @@ - + + + + + - diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs index e55f07cec2..e5d1a58b5b 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/App.xaml.cs @@ -2,8 +2,12 @@ // 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.CommandPalette.UI.Helpers; +using Microsoft.CommandPalette.UI.Models; +using Microsoft.CommandPalette.UI.Pages; using Microsoft.CommandPalette.UI.Services; +using Microsoft.CommandPalette.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerToys.Telemetry; @@ -66,6 +70,23 @@ public partial class App : Application services.AddSingleton(logger); services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext()); + // Register settings & app state + var settingsModel = SettingsModel.LoadSettings(logger); + services.AddSingleton(settingsModel); + + var appStateModel = AppStateModel.LoadState(logger); + services.AddSingleton(appStateModel); + + // Register services + services.AddSingleton(); + + // Register view models + services.AddSingleton(); + + // Register views + services.AddSingleton(); + services.AddSingleton(); + // Register services return services.BuildServiceProvider(); } @@ -76,7 +97,11 @@ public partial class App : Application /// Details about the launch request and process. protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { - AppWindow = new MainWindow(logger); - AppWindow.Activate(); + var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs(); + + var mainWindow = Services.GetRequiredService(); + AppWindow = mainWindow; + + ((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs); } } diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml index 61d209f7e3..be1f936653 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml @@ -1,22 +1,16 @@ - - - - - - - - - - - + xmlns:pages="using:Microsoft.CommandPalette.UI.Pages" + xmlns:winuiex="using:WinUIEx" + Width="800" + Height="480" + MinWidth="320" + MinHeight="240" + mc:Ignorable="d"> + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs index 8473b4253f..b71dda9e25 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/MainWindow.xaml.cs @@ -3,24 +3,29 @@ // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; +using Microsoft.CommandPalette.UI.Pages; using Microsoft.Extensions.Logging; -using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; using Windows.ApplicationModel.Activation; +using WinUIEx; namespace Microsoft.CommandPalette.UI; /// /// An empty window that can be used on its own or navigated to within a Frame. /// -public sealed partial class MainWindow : Window +public sealed partial class MainWindow : WindowEx { private readonly ILogger logger; + private readonly ShellPage shellPage; - public MainWindow(ILogger logger) + public MainWindow(ShellPage shellPage, ILogger logger) { InitializeComponent(); this.logger = logger; + this.shellPage = shellPage; + + RootElement.Content = this.shellPage; } public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs) diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj index 5a4b0cb148..8bbabf8088 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj @@ -60,12 +60,16 @@ + + + + @@ -157,5 +161,12 @@ + + + + + + MSBuild:Compile + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Package-Dev.appxmanifest b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Package-Dev.appxmanifest index f8a16a433b..ab32e4a8c7 100644 --- a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Package-Dev.appxmanifest +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Package-Dev.appxmanifest @@ -28,7 +28,7 @@ ms-resource:AppNameDev A Lone Developer - Assets\StoreLogo.png + Assets\Dev\StoreLogo.png disabled @@ -49,10 +49,10 @@ DisplayName="ms-resource:AppNameDev" Description="ms-resource:AppDescriptionDev" BackgroundColor="transparent" - Square150x150Logo="Assets\Square150x150Logo.png" - Square44x44Logo="Assets\Square44x44Logo.png"> - - + Square150x150Logo="Assets\Dev\Square150x150Logo.png" + Square44x44Logo="Assets\Dev\Square44x44Logo.png"> + + @@ -72,7 +72,7 @@ - Assets\StoreLogo.png + Assets\Dev\StoreLogo.png Command Palette Dev URI scheme diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml new file mode 100644 index 0000000000..6e0cdaef26 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.cs new file mode 100644 index 0000000000..c93f84e6a5 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Pages/ShellPage.xaml.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 Microsoft.CommandPalette.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CommandPalette.UI.Pages; + +public sealed partial class ShellPage : Page +{ + private readonly ShellViewModel viewModel; + + public ShellPage(ShellViewModel viewModel) + { + InitializeComponent(); + this.viewModel = viewModel; + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Services/TrayIconService.cs b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Services/TrayIconService.cs new file mode 100644 index 0000000000..e65cde76fb --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Services/TrayIconService.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation +// The 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.Runtime.InteropServices; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.Models.Messages; +using Microsoft.CommandPalette.UI.Models; +using Microsoft.UI.Xaml; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; +using RS_ = Microsoft.CommandPalette.UI.Helpers.ResourceLoaderInstance; + +namespace Microsoft.CommandPalette.UI.Helpers; + +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_*")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_*")] +internal sealed partial class TrayIconService +{ + private const uint MY_NOTIFY_ID = 1000; + private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1; + + private readonly SettingsModel _settingsModel; + private readonly uint WM_TASKBAR_RESTART; + + private Window? _window; + private HWND _hwnd; + private WNDPROC? _originalWndProc; + private WNDPROC? _trayWndProc; + private NOTIFYICONDATAW? _trayIconData; + private DestroyIconSafeHandle? _largeIcon; + private DestroyMenuSafeHandle? _popupMenu; + + public TrayIconService(SettingsModel settingsModel) + { + _settingsModel = settingsModel; + + // 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"); + } + + public void SetupTrayIcon(bool? showSystemTrayIcon = null) + { + if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon) + { + if (_window is null) + { + _window = new Window(); + _hwnd = new HWND(WindowNative.GetWindowHandle(_window)); + + // 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**. + _trayWndProc = WindowProc; + var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_trayWndProc); + _originalWndProc = Marshal.GetDelegateForFunctionPointer(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer)); + } + + if (_trayIconData is 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(), + 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 + PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d); + + if (_popupMenu is null) + { + _popupMenu = PInvoke.CreatePopupMenu_SafeHandle(); + PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings")); + PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Close")); + } + } + else + { + Destroy(); + } + } + + public void Destroy() + { + if (_trayIconData is not null) + { + var d = (NOTIFYICONDATAW)_trayIconData; + if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d)) + { + _trayIconData = null; + } + } + + if (_popupMenu is not null) + { + _popupMenu.Close(); + _popupMenu = null; + } + + if (_largeIcon is not null) + { + _largeIcon.Close(); + _largeIcon = null; + } + + if (_window is not null) + { + _window.Close(); + _window = null; + _hwnd = HWND.Null; + } + } + + private DestroyIconSafeHandle GetAppIconHandle() + { + var exePath = Path.Combine(AppContext.BaseDirectory, "Microsoft.CommandPalette.UI.exe"); + DestroyIconSafeHandle largeIcon; + PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out _, 1); + return largeIcon; + } + + private LRESULT WindowProc( + HWND hwnd, + uint uMsg, + WPARAM wParam, + LPARAM lParam) + { + switch (uMsg) + { + case PInvoke.WM_COMMAND: + { + if (wParam == PInvoke.WM_USER + 1) + { + WeakReferenceMessenger.Default.Send(); + } + else if (wParam == PInvoke.WM_USER + 2) + { + WeakReferenceMessenger.Default.Send(); + } + } + + break; + + // 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 (_trayIconData is null) + { + SetupTrayIcon(); + } + } + + 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 + SetupTrayIcon(); + } + else if (uMsg == WM_TRAY_ICON) + { + switch ((uint)lParam.Value) + { + case PInvoke.WM_RBUTTONUP: + { + if (_popupMenu is not null) + { + PInvoke.GetCursorPos(out var cursorPos); + PInvoke.SetForegroundWindow(_hwnd); + PInvoke.TrackPopupMenuEx(_popupMenu, (uint)TRACK_POPUP_MENU_FLAGS.TPM_LEFTALIGN | (uint)TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN, cursorPos.X, cursorPos.Y, _hwnd, null); + } + } + + break; + case PInvoke.WM_LBUTTONUP: + case PInvoke.WM_LBUTTONDBLCLK: + WeakReferenceMessenger.Default.Send(new(string.Empty, HWND.Null)); + break; + } + } + + break; + } + + return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam); + } +} diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Strings/en-us/Resources.resw b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..ef4458d803 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Strings/en-us/Resources.resw @@ -0,0 +1,559 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Fallback commands + A section header for information about the commands presented to the user when the search text doesn't exactly match the name of a command. + + + This extension is disabled + A header to inform the user that an extension is not currently active + + + Enable this extension to view commands and settings + Additional details for when an extension is disabled. Displayed under ExtensionDisabledHeader.Text + + + Disabled + Displayed when an extension is disabled + + + Enable + Displayed on a toggle controlling the extension's enabled / disabled state + + + Load commands and settings from this extension + Displayed on a toggle controlling the extension's enabled / disabled state + + + 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 + + + Global hotkey + + + Directly open Command Palette to this command. + + + Alias + + + A short keyword used to navigate to this command. + + + Alias activation + + + Choose when the alias runs. Direct runs as soon as you type the alias. Indirect runs after a trailing space. + + + Built-in + + + These commands are built-in to the Windows Command Palette. + + + Activation key + + + This key will open the Command Palette. + + + Use low-level keyboard hook + + + Try this if there are issues with the shortcut (Command Palette might not get focus when triggered from an elevated window) + + + Ignore shortcut in fullscreen mode + + + Preventing disruption of the program running in fullscreen by unintentional activation of shortcut + + + Highlight search on activate + + + Selects the previous search text at launch + + + Show app details + + + Controls if app details are automatically expanded or not. + + + Backspace goes back + + + If the search field is empty, backspace returns to the previous page + + + Single-click activation + + + Choose how to interact with list items: single-click to activate, or single-click to select and double-click to activate + + + Windows Command Palette + + + © 2025. All rights reserved. + + + View GitHub repository + + + Extension SDK docs + + + Send feedback + + + General + + + Extensions + + + Open Command Palette settings + + + Activation + + + Behavior + + + Search commands... + + + Show system tray icon + + + Choose if Command Palette is visible in the system tray + + + Disable animations + + + Disable animations when switching between pages + + + Back + + + Back (Alt + Left arrow) + + + More + + + Last position + Reopen the window where it was last closed + + + Settings + + + Close + Close as a verb, as in Close the application + + + Direct + + + Indirect + + + Enter alias + + + Show status messages + + + Show status messages + + + Navigation pane closed + + + Navigation page opened + + + Enable external reload + + + Trigger reload of the extension externally with the x-cmdpal://reload command + + + For developers + + + an untitled + + + Navigated to {0} page + + + Settings (Ctrl+,) + + + No extensions found + + + Try a different search term + + + More options + + + Reload extensions + + + Reloading extensions.. + + + Discover more extensions + + + Find more extensions on the Microsoft Store or WinGet. + + + Learn how to create your own extensions + + + Find extensions on the Microsoft Store + + + Microsoft Store + + + Find extensions on WinGet + + + Microsoft Store + + + Search extensions + + + Command Palette has encountered a fatal error and must close. + + + Command Palette - Fatal error + + + Never + + + Immediately + + + 10 seconds + + + 20 seconds + + + 30 seconds + + + 60 seconds + + + 90 seconds + + + 2 minutes + + + 3 minutes + + + Automatically return home + + + Automatically returns to home page after a period of inactivity when Command Palette is closed + + + General + + + Extensions + + \ No newline at end of file diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Colors.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Colors.xaml new file mode 100644 index 0000000000..edca3f479c --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Colors.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Settings.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Settings.xaml new file mode 100644 index 0000000000..89c01814eb --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/Settings.xaml @@ -0,0 +1,25 @@ + + + + + + + + 4 + + + + + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBlock.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBlock.xaml new file mode 100644 index 0000000000..6160585127 --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBlock.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBox.xaml b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBox.xaml new file mode 100644 index 0000000000..167636e8ec --- /dev/null +++ b/src/modules/Deux/UI/Microsoft.CommandPalette.UI/Styles/TextBox.xaml @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + +