Add the Command Palette module (#37908)

Windows Command Palette ("CmdPal") is the next iteration of PowerToys Run. With extensibility at its core, the Command Palette is your one-stop launcher to start _anything_.

By default, CmdPal is bound to <kbd>Win+Alt+Space</kbd>.

![cmdpal-pr-002](https://github.com/user-attachments/assets/5077ec04-1009-478a-92d6-0a30989d44ac)
![cmdpal-pr-003](https://github.com/user-attachments/assets/63b4762a-9c19-48eb-9242-18ea48240ba0)

----

This brings the current preview version of CmdPal into the upstream PowerToys repo. There are still lots of bugs to work out, but it's reached the state we're ready to start sharing it with the world. From here, we can further collaborate with the community on the features that are important, and ensuring that we've got a most robust API to enable developers to build whatever extensions they want. 

Most of the built-in PT Run modules have already been ported to CmdPal's extension API. Those include:
* Installed apps
* Shell commands
* File search (powered by the indexer)
* Windows Registry search
* Web search
* Windows Terminal Profiles
* Windows Services
* Windows settings


There are a couple new extensions built-in
* You can now search for packages on `winget` and install them right from the palette. This also powers searching for extensions for the palette
* The calculator has an entirely new implementation. This is currently less feature complete than the original PT Run one - we're looking forward to updating it to be more complete for future ingestion in Windows
* "Bookmarks" allow you to save shortcuts to files, folders, and webpages as top-level commands in the palette. 

We've got a bunch of other samples too, in this repo and elsewhere

### PowerToys specific notes

CmdPal will eventually graduate out of PowerToys to live as its own application, which is why it's implemented just a little differently than most other modules. Enabling CmdPal will install its `msix` package. 

The CI was minorly changed to support CmdPal version numbers independent of PowerToys itself. It doesn't make sense for us to start CmdPal at v0.90, and in the future, we want to be able to rev CmdPal independently of PT itself. 


Closes #3200, closes #3600, closes #7770, closes #34273, closes #36471, closes #20976, closes #14495
  
  
-----

TODOs et al


**Blocking:**
- [ ] Images and descriptions in Settings and OOBE need to be properly defined, as mentioned before
  - [ ] Niels is on it
- [x] Doesn't start properly from PowerToys unless the fix PR is merged.
  - https://github.com/zadjii-msft/PowerToys/pull/556 merged
- [x] I seem to lose focus a lot when I press on some limits, like between the search bar and the results.
  - This is https://github.com/zadjii-msft/PowerToys/issues/427
- [x] Turned off an extension like Calculator and it was still working.
  - Need to get rid of that toggle, it doesn't do anything currently
- [x] `ListViewModel.<FetchItems>` crash
  - Pretty confident that was fixed in https://github.com/zadjii-msft/PowerToys/pull/553

**Not blocking / improvements:**
- Show the shortcut through settings, as mentioned before, or create a button that would open CmdPalette settings.
- When PowerToys starts, CmdPalette is always shown if enabled. That's weird when just starting PowerToys/ logging in to the computer with PowerToys auto-start activated. I think this should at least be a setting.
- Needing to double press a result for it to do the default action seems quirky. If one is already selected, I think just pressing should be enough for it to do the action.
  - This is currently a setting, though we're thinking of changing the setting even more: https://github.com/zadjii-msft/PowerToys/issues/392
- There's no URI extension. Was surprised when typing a URL that it only proposed a web search.
- [x] There's no System commands extension. Was expecting to be able to quickly restart the computer by typing restart but it wasn't there.
  - This is in PR https://github.com/zadjii-msft/PowerToys/pull/452  
  
---------

Co-authored-by: joadoumie <98557455+joadoumie@users.noreply.github.com>
Co-authored-by: Jordi Adoumie <jordiadoumie@microsoft.com>
Co-authored-by: Mike Griese <zadjii@gmail.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Michael Hawker <24302614+michael-hawker@users.noreply.github.com>
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
Co-authored-by: Seraphima <zykovas91@gmail.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com>
Co-authored-by: Eric Johnson <ericjohnson327@gmail.com>
Co-authored-by: Ethan Fang <ethanfang@microsoft.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
This commit is contained in:
Mike Griese
2025-03-19 03:39:57 -05:00
committed by GitHub
parent a62acf7a71
commit f68f408be3
984 changed files with 69758 additions and 277 deletions

View File

@@ -69,6 +69,10 @@
<PackageReference Include="WinUIEx" />
<!-- HACK: To make sure the version pulled in by Microsoft.Extensions.Hosting is current. -->
<PackageReference Include="System.Text.Json" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -2,7 +2,7 @@
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>EnvironmentVariables</RootNamespace>
@@ -67,6 +67,10 @@
<PackageReference Include="WinUIEx" />
<!-- HACK: To make sure the version pulled in by Microsoft.Extensions.Hosting is current. -->
<PackageReference Include="System.Text.Json" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -83,7 +83,7 @@ public:
std::wstring path = get_module_folderpath(globals::instance);
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
if (!package::IsPackageRegistered(constants::nonlocalizable::ContextMenuPackageName))
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
{
package::RegisterSparsePackage(path, packageUri);
}

View File

@@ -63,6 +63,10 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="WinUIEx" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -20,6 +20,8 @@
<!-- This package is a dependency of Microsoft.Extensions.Logging.EventLog, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
</ItemGroup>
<ItemGroup>

View File

@@ -66,6 +66,10 @@
<PackageReference Include="WinUIEx" />
<!-- HACK: To make sure the version pulled in by Microsoft.Extensions.Hosting is current. -->
<PackageReference Include="System.Text.Json" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -202,7 +202,9 @@ namespace HostsUILib.ViewModels
}
_entries.CollectionChanged += Entries_CollectionChanged;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
Entries = new AdvancedCollectionView(_entries, true);
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending));
ApplyFilters();
OnPropertyChanged(nameof(Entries));

View File

@@ -140,7 +140,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2739.15\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2739.15\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
@@ -152,7 +152,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2739.15\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2739.15\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets'))" />
</Target>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2739.15" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />

View File

@@ -13,7 +13,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<UseWinUI>true</UseWinUI>
<Platforms>x64;ARM64</Platforms>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
@@ -55,6 +55,8 @@
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="WinUIEx" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -155,7 +155,7 @@ namespace newplus::utilities
static const auto new_dll_path = get_module_folderpath(module_instance_handle);
auto new_package_uri = new_dll_path + L"\\" + constants::non_localizable::msix_package_name;
if (!package::IsPackageRegistered(constants::non_localizable::context_menu_package_name))
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::non_localizable::context_menu_package_name))
{
package::RegisterSparsePackage(new_dll_path, new_package_uri);
}

View File

@@ -0,0 +1,31 @@
{
"$version": "1.0.0",
"snippets":
[
{
"input": "pwsh -c .\\doc\\initial-sdk-spec\\generate-interface.ps1 > .\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.idl",
"name": "Generate interface",
"description": "Generate the interface from the SDK spec\nThis drops it into Microsoft.CommandPalette.Extensions"
},
{
"input": "tasklist | findstr Extension",
"name": "List running extensions",
"description": "This will list all running extensions, as long as they have 'Extension' in the name (they should)"
},
{
"input": "for /F \"tokens=2\" %A in ('tasklist ^| findstr Extension') do taskkill /PID %A /F",
"name": "🚨 Terminate extensions 🚨",
"description": "Terminate anything with 'Extension' in the name"
},
{
"input": "start https://github.com/zadjii-msft/PowerToys/compare/main...zadjii-msft:PowerToys:{branch}?expand=1\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D",
"name": "New PR",
"description": "Create a new PR targeting the right fork.\nReplace {branch} with the actual branch you want to merge."
},
{
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
"name": "Update template project",
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
}
]
}

View File

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

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8}</ProjectGuid>
<RootNamespace>CmdPalModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<TargetName>PowerToys.CmdPalModuleInterface</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdPalModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,268 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/resources.h>
#include <common/utils/package.h>
#include <common/utils/process_path.h>
#include <Psapi.h>
#include <TlHelp32.h>
HINSTANCE g_hInst_cmdPal = 0;
BOOL APIENTRY DllMain(HMODULE hInstance,
DWORD ul_reason_for_call,
LPVOID)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hInst_cmdPal = hInstance;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
class CmdPal : public PowertoyModuleIface
{
private:
bool m_enabled = false;
std::wstring app_name;
//contains the non localized key of the powertoy
std::wstring app_key;
void LaunchApp()
{
auto package = package::GetRegisteredPackage(L"Microsoft.CommandPalette", false);
if (package.has_value())
{
auto getAppListEntriesOperation = package->GetAppListEntriesAsync();
auto appEntries = getAppListEntriesOperation.get();
if (appEntries.Size() > 0)
{
winrt::Windows::Foundation::IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
launchOperation.get();
}
else
{
Logger::error(L"No app entries found for the package.");
}
}
else
{
Logger::error(L"CmdPal package is not registered.");
}
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
{
std::vector<DWORD> processIds;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
processIds.push_back(processEntry.th32ProcessID);
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return processIds;
}
void TerminateCmdPal()
{
auto processIds = GetProcessesIdByName(L"Microsoft.CmdPal.UI.exe");
if (processIds.size() == 0)
{
Logger::trace(L"Nothing To PROCESS_TERMINATE");
return;
}
for (DWORD pid : processIds)
{
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL)
{
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
}
}
}
public:
CmdPal()
{
app_name = L"CmdPal";
app_key = L"CmdPal";
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "CmdPal");
}
~CmdPal()
{
if (m_enabled)
{
}
m_enabled = false;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
Logger::trace("CmdPal::destroy()");
TerminateCmdPal();
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return app_name.c_str();
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredCmdPalEnabledValue();
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
values.save_to_settings_file();
// Otherwise call a custom function to process the settings before saving them to disk:
// save_settings();
}
catch (std::exception&)
{
// Improper JSON.
}
}
virtual void enable()
{
Logger::trace("CmdPal::enable()");
m_enabled = true;
try
{
if (!package::GetRegisteredPackage(L"Microsoft.CommandPalette", false).has_value())
{
Logger::info(L"CmdPal not installed. Installing...");
std::wstring installationFolder = get_module_folderpath();
#if _DEBUG
std::wstring archSubdir = L"x64";
#ifdef _M_ARM64
archSubdir = L"ARM64";
#endif
auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\", false);
auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\Dependencies\\" + archSubdir + L"\\", true);
#else
auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\", false);
auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\Dependencies\\", true);
#endif
if (!msix.empty())
{
auto msixPath = msix[0];
if (!package::RegisterPackage(msixPath, dependencies))
{
Logger::error(L"Failed to install CmdPal package");
}
}
}
}
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
}
LaunchApp();
}
virtual void disable()
{
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
m_enabled = false;
}
virtual bool on_hotkey(size_t) override
{
return false;
}
virtual size_t get_hotkeys(Hotkey*, size_t) override
{
return 0;
}
virtual bool is_enabled() override
{
return m_enabled;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
}

View File

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

View File

@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

View File

@@ -0,0 +1,16 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
#include <string>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.System.h>
#endif //PCH_H

View File

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

View File

@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<Platforms>x64;ARM64</Platforms>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>Recommended</AnalysisMode>
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
<NuGetAuditMode>direct</NuGetAuditMode>
<PlatformTarget>$(Platform)</PlatformTarget>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.1.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Text.Json" Version="9.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35507.96 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemplateCmdPalExtension", "TemplateCmdPalExtension\TemplateCmdPalExtension.csproj", "{79F86DE5-70B1-4EC1-9832-DF428B55E466}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.ActiveCfg = Debug|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.Build.0 = Debug|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|ARM64.Deploy.0 = Debug|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.ActiveCfg = Debug|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.Build.0 = Debug|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x64.Deploy.0 = Debug|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.ActiveCfg = Debug|x86
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.Build.0 = Debug|x86
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Debug|x86.Deploy.0 = Debug|x86
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.ActiveCfg = Release|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.Build.0 = Release|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|ARM64.Deploy.0 = Release|ARM64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.ActiveCfg = Release|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.Build.0 = Release|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x64.Deploy.0 = Release|x64
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.ActiveCfg = Release|x86
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.Build.0 = Release|x86
{79F86DE5-70B1-4EC1-9832-DF428B55E466}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CEDBC581-5818-4350-BC8A-A1ECE687D357}
EndGlobalSection
EndGlobal

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap3 rescap">
<Identity
Name="TemplateCmdPalExtension"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="0.0.1.0" />
<!-- When you're ready to publish your extension, you'll need to change the
Publisher= to match your own identity -->
<Properties>
<DisplayName>TemplateDisplayName</DisplayName>
<PublisherDisplayName>A Lone Developer</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="TemplateDisplayName"
Description="TemplateDisplayName"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="TemplateCmdPalExtension.exe" Arguments="-RegisterProcessAsComServer" DisplayName="TemplateDisplayName">
<com:Class Id="FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" DisplayName="TemplateDisplayName" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.commandpalette"
Id="ID"
PublicFolder="Public"
DisplayName="TemplateDisplayName"
Description="TemplateDisplayName">
<uap3:Properties>
<CmdPalProvider>
<Activation>
<CreateInstance ClassId="FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" />
</Activation>
<SupportedInterfaces>
<Commands/>
</SupportedInterfaces>
</CmdPalProvider>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace TemplateCmdPalExtension;
internal sealed partial class TemplateCmdPalExtensionPage : ListPage
{
public TemplateCmdPalExtensionPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "TemplateDisplayName";
Name = "Open";
}
public override IListItem[] GetItems()
{
return [
new ListItem(new NoOpCommand()) { Title = "TODO: Implement your extension here" }
];
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Microsoft.CommandPalette.Extensions;
namespace TemplateCmdPalExtension;
public class Program
{
[MTAThread]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
using ExtensionServer server = new();
var extensionDisposedEvent = new ManualResetEvent(false);
var extensionInstance = new TemplateCmdPalExtension(extensionDisposedEvent);
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
server.RegisterExtension(() => extensionInstance);
// This will make the main thread wait until the event is signalled by the extension class.
// Since we have single instance of the extension object, we exit as soon as it is disposed.
extensionDisposedEvent.WaitOne();
}
else
{
Console.WriteLine("Not being launched as a Extension... exiting.");
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>ARM64</Platform>
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
<PublishDir>bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
<PublishReadyToRun>True</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x64</Platform>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishDir>bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
<PublishReadyToRun>True</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"TemplateCmdPalExtension (Package)": {
"commandName": "MsixPackage",
"doNotLaunchApp": true
},
"TemplateCmdPalExtension (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CommandPalette.Extensions;
namespace TemplateCmdPalExtension;
[ComVisible(true)]
[Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")]
[ComDefaultInterface(typeof(IExtension))]
public sealed partial class TemplateCmdPalExtension : IExtension, IDisposable
{
private readonly ManualResetEvent _extensionDisposedEvent;
private readonly TemplateCmdPalExtensionCommandsProvider _provider = new();
public TemplateCmdPalExtension(ManualResetEvent extensionDisposedEvent)
{
this._extensionDisposedEvent = extensionDisposedEvent;
}
public object? GetProvider(ProviderType providerType)
{
return providerType switch
{
ProviderType.Commands => _provider,
_ => null,
};
}
public void Dispose() => this._extensionDisposedEvent.Set();
}

View File

@@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>TemplateCmdPalExtension</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CommandPalette.Extensions" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
<CsWinRTAotOptimizerEnabled>true</CsWinRTAotOptimizerEnabled>
<CsWinRTAotWarningLevel>2</CsWinRTAotWarningLevel>
<!-- Suppress DynamicallyAccessedMemberTypes.PublicParameterlessConstructor in fallback code path of Windows SDK projection -->
<WarningsNotAsErrors>IL2081</WarningsNotAsErrors>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace TemplateCmdPalExtension;
public partial class TemplateCmdPalExtensionCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
public TemplateCmdPalExtensionCommandsProvider()
{
DisplayName = "TemplateDisplayName";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
_commands = [
new CommandItem(new TemplateCmdPalExtensionPage()) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="TemplateCmdPalExtension.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
public partial class AllAppsCommandProvider : CommandProvider
{
public static readonly AllAppsPage Page = new();
private readonly CommandItem _listItem;
public AllAppsCommandProvider()
{
Id = "AllApps";
DisplayName = Resources.installed_apps;
Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
Settings = AllAppsSettings.Instance.Settings;
_listItem = new(Page) { Subtitle = Resources.search_installed_apps };
}
public override ICommandItem[] TopLevelCommands() => [_listItem];
public ICommandItem? LookupApp(string displayName)
{
var items = Page.GetItems();
// We're going to do this search in two directions:
// First, is this name a substring of any app...
var nameMatches = items.Where(i => i.Title.Contains(displayName));
// ... Then, does any app have this name as a substring ...
// Only get one of these - "Terminal Preview" contains both "Terminal" and "Terminal Preview", so just take the best one
var appMatches = items.Where(i => displayName.Contains(i.Title)).OrderByDescending(i => i.Title.Length).Take(1);
// ... Now, combine those two
var both = nameMatches.Concat(appMatches);
if (both.Count() == 1)
{
return both.First();
}
else if (nameMatches.Count() == 1 && appMatches.Count() == 1)
{
if (nameMatches.First() == appMatches.First())
{
return nameMatches.First();
}
}
return null;
}
}

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage
{
private readonly Lock _listLock = new();
private AppListItem[] allAppsSection = [];
public AllAppsPage()
{
this.Name = Resources.all_apps;
this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
this.ShowDetails = true;
this.IsLoading = true;
this.PlaceholderText = Resources.search_installed_apps_placeholder;
Task.Run(() =>
{
lock (_listLock)
{
BuildListItems();
}
});
}
public override IListItem[] GetItems()
{
if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
{
BuildListItems();
}
}
return allAppsSection;
}
private void BuildListItems()
{
this.IsLoading = true;
Stopwatch stopwatch = new();
stopwatch.Start();
List<AppItem> apps = GetPrograms();
this.allAppsSection = apps
.Select((app) => new AppListItem(app, true))
.ToArray();
this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag();
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
}
internal List<AppItem> GetPrograms()
{
IEnumerable<AppItem> uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
.Select(app =>
new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
});
IEnumerable<AppItem> win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app =>
{
string icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
app.IcoPath;
// icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = app.Type(),
IcoPath = icoPath,
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
DirPath = app.Location,
Commands = app.GetCommands(),
};
});
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Storage;
using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps;
public sealed class AppCache : IDisposable
{
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
private PackageRepository _packageRepository;
private Win32ProgramRepository _win32ProgramRepository;
private bool _disposed;
public IList<Win32Program> Win32s => _win32ProgramRepository.Items;
public IList<UWPApplication> UWPs => _packageRepository.Items;
public static readonly Lazy<AppCache> Instance = new(() => new());
public AppCache()
{
_win32ProgramRepositoryHelper = new Win32ProgramFileSystemWatchers();
_win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper.FileSystemWatchers.Cast<IFileSystemWatcherWrapper>().ToList(), AllAppsSettings.Instance, _win32ProgramRepositoryHelper.PathsToWatch);
_packageRepository = new PackageRepository(new PackageCatalogWrapper());
var a = Task.Run(() =>
{
_win32ProgramRepository.IndexPrograms();
});
var b = Task.Run(() =>
{
_packageRepository.IndexPrograms();
UpdateUWPIconPath(ThemeHelper.GetCurrentTheme());
});
Task.WaitAll(a, b);
AllAppsSettings.Instance.LastIndexTime = DateTime.Today;
}
private void UpdateUWPIconPath(Theme theme)
{
if (_packageRepository != null)
{
foreach (UWPApplication app in _packageRepository)
{
app.UpdateLogoPath(theme);
}
}
}
public bool ShouldReload() => _packageRepository.ShouldReload() || _win32ProgramRepository.ShouldReload();
public void ResetReloadFlag()
{
_packageRepository.ResetReloadFlag();
_win32ProgramRepository.ResetReloadFlag();
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_win32ProgramRepositoryHelper?.Dispose();
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
internal sealed class AppItem
{
public string Name { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string IcoPath { get; set; } = string.Empty;
public string ExePath { get; set; } = string.Empty;
public string DirPath { get; set; } = string.Empty;
public string UserModelId { get; set; } = string.Empty;
public bool IsPackaged { get; set; }
public List<CommandContextItem>? Commands { get; set; }
public AppItem()
{
}
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
internal sealed partial class AppListItem : ListItem
{
private readonly AppItem _app;
private static readonly Tag _appTag = new("App");
private readonly Lazy<Details> _details;
private readonly Lazy<IconInfo> _icon;
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public AppListItem(AppItem app, bool useThumbnails)
: base(new AppCommand(app))
{
_app = app;
Title = app.Name;
Subtitle = app.Subtitle;
Tags = [_appTag];
MoreCommands = _app.Commands!.ToArray();
_details = new Lazy<Details>(() => BuildDetails());
_icon = new Lazy<IconInfo>(() =>
{
var t = FetchIcon(useThumbnails);
t.Wait();
return t.Result;
});
}
private Details BuildDetails()
{
var metadata = new List<DetailsElement>();
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
if (!_app.IsPackaged)
{
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
}
return new Details()
{
Title = this.Title,
HeroImage = this.Icon ?? new IconInfo(string.Empty),
Metadata = metadata.ToArray(),
};
}
public async Task<IconInfo> FetchIcon(bool useThumbnails)
{
IconInfo? icon = null;
if (_app.IsPackaged)
{
icon = new IconInfo(_app.IcoPath);
if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}
return icon;
}
if (useThumbnails)
{
try
{
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch
{
}
icon = icon ?? new IconInfo(_app.IcoPath);
}
else
{
icon = new IconInfo(_app.IcoPath);
}
if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}
return icon;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,29 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1825_17772)">
<path d="M7.2002 8H12.8002C13.242 8 13.6002 8.35817 13.6002 8.8V13.6C13.6002 14.0418 13.242 14.4 12.8002 14.4H7.2002V8Z" fill="url(#paint0_linear_1825_17772)"/>
<path d="M0.799805 8H7.1998V14.4H1.59981C1.15798 14.4 0.799805 14.0418 0.799805 13.6V8Z" fill="url(#paint1_linear_1825_17772)"/>
<path d="M0.799805 2.39961C0.799805 1.95778 1.15798 1.59961 1.5998 1.59961H6.3998C6.84163 1.59961 7.1998 1.95778 7.1998 2.39961V7.99961H0.799805V2.39961Z" fill="url(#paint2_linear_1825_17772)"/>
<rect width="6.4" height="6.4" rx="0.8" transform="matrix(0.701061 0.713102 -0.701061 0.713102 10.7998 0)" fill="url(#paint3_linear_1825_17772)"/>
</g>
<defs>
<linearGradient id="paint0_linear_1825_17772" x1="12.5686" y1="15.0983" x2="7.57856" y2="7.74389" gradientUnits="userSpaceOnUse">
<stop stop-color="#5C6166"/>
<stop offset="1" stop-color="#8A9299"/>
</linearGradient>
<linearGradient id="paint1_linear_1825_17772" x1="6.36596" y1="14.8714" x2="1.98416" y2="7.32118" gradientUnits="userSpaceOnUse">
<stop stop-color="#2E3133"/>
<stop offset="1" stop-color="#5C6166"/>
</linearGradient>
<linearGradient id="paint2_linear_1825_17772" x1="6.17254" y1="8.59747" x2="2.16557" y2="1.7111" gradientUnits="userSpaceOnUse">
<stop stop-color="#8A9299"/>
<stop offset="1" stop-color="#A1AAB3"/>
</linearGradient>
<linearGradient id="paint3_linear_1825_17772" x1="6.75524" y1="3.90242" x2="-0.056471" y2="3.62557" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="1" stop-color="#3CCBF4"/>
</linearGradient>
<clipPath id="clip0_1825_17772">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Apps</RootNamespace>
<Nullable>enable</Nullable>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="WyHash" />
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Remove="Assets\AllApps.png" />
<None Remove="Assets\AllApps.svg" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AllApps.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AllApps.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
GetPhysicallyInstalledSystemMemory
GlobalMemoryStatusEx
GetSystemInfo
CoCreateInstance
SetForegroundWindow
IsIconic
RegisterHotKey
SetWindowLongPtr
CallWindowProc
ShowWindow
SetForegroundWindow
SetFocus
SetActiveWindow
MonitorFromWindow
GetMonitorInfo
SHCreateStreamOnFileEx
CoAllowSetForegroundWindow
SHCreateStreamOnFileEx
SHLoadIndirectString

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781")]
[ComImport]
public class AppxFactory
{
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Windows.Win32.System.Com;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public static class AppxPackageHelper
{
private static readonly IAppxFactory AppxFactory = (IAppxFactory)new AppxFactory();
// This function returns a list of attributes of applications
internal static IEnumerable<IAppxManifestApplication> GetAppsFromManifest(IStream stream)
{
var reader = AppxFactory.CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
while (manifestApps.GetHasCurrent())
{
var manifestApp = manifestApps.GetCurrent();
var hr = manifestApp.GetStringValue("AppListEntry", out var appListEntry);
_ = CheckHRAndReturnOrThrow(hr, appListEntry);
if (appListEntry != "none")
{
yield return manifestApp;
}
manifestApps.MoveNext();
}
}
internal static T CheckHRAndReturnOrThrow<T>(HRESULT hr, T result)
{
if (hr != HRESULT.S_OK)
{
Marshal.ThrowExceptionForHR((int)hr);
}
return result;
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs
[Flags]
public enum ActivateOptions
{
None = 0x00000000,
DesignMode = 0x00000001,
NoErrorUI = 0x00000002,
NoSplashScreen = 0x00000004,
}
// ApplicationActivationManager
[ComImport]
[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IApplicationActivationManager
{
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
// Application Activation Manager Class
[ComImport]
[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
public class ApplicationActivationManager : IApplicationActivationManager
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using Windows.Win32.System.Com;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxFactory
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap0_2(); // skip 2 methods
internal IAppxManifestReader CreateManifestReader(IStream inputStream);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplication
{
[PreserveSig]
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
[PreserveSig]
HRESULT GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplicationsEnumerator
{
IAppxManifestApplication GetCurrent();
bool GetHasCurrent();
bool MoveNext();
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestProperties
{
[PreserveSig]
HRESULT GetBoolValue([MarshalAs(UnmanagedType.LPWStr)] string name, out bool value);
[PreserveSig]
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestReader
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap0_1(); // skip 1 method
IAppxManifestProperties GetProperties();
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap1_5(); // skip 5 methods
IAppxManifestApplicationsEnumerator GetApplications();
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public interface IPackage
{
string Name { get; }
string FullName { get; }
string FamilyName { get; }
bool IsFramework { get; }
bool IsDevelopmentMode { get; }
string InstalledLocation { get; }
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public interface IPackageManager
{
IEnumerable<IPackage> FindPackagesForCurrentUser();
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public interface IProgram
{
string UniqueIdentifier { get; set; }
string Name { get; }
string Description { get; set; }
string Location { get; }
bool Enabled { get; set; }
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public enum LogoType
{
Error,
Colored,
HighContrast,
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using Windows.Management.Deployment;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public class PackageManagerWrapper : IPackageManager
{
private readonly PackageManager _packageManager;
public PackageManagerWrapper()
{
_packageManager = new PackageManager();
}
public IEnumerable<IPackage> FindPackagesForCurrentUser()
{
var user = WindowsIdentity.GetCurrent().User;
if (user != null)
{
var pkgs = _packageManager.FindPackagesForUser(user.Value);
return pkgs.Select(PackageWrapper.GetWrapperFromPackage).Where(package => package != null);
}
return Enumerable.Empty<IPackage>();
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Windows.Foundation.Metadata;
using Package = Windows.ApplicationModel.Package;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public class PackageWrapper : IPackage
{
public string Name { get; } = string.Empty;
public string FullName { get; } = string.Empty;
public string FamilyName { get; } = string.Empty;
public bool IsFramework { get; }
public bool IsDevelopmentMode { get; }
public string InstalledLocation { get; } = string.Empty;
public PackageWrapper()
{
}
public PackageWrapper(string name, string fullName, string familyName, bool isFramework, bool isDevelopmentMode, string installedLocation)
{
Name = name;
FullName = fullName;
FamilyName = familyName;
IsFramework = isFramework;
IsDevelopmentMode = isDevelopmentMode;
InstalledLocation = installedLocation;
}
private static readonly Lazy<bool> IsPackageDotInstallationPathAvailable = new(() =>
ApiInformation.IsPropertyPresent(typeof(Package).FullName, nameof(Package.InstalledLocation.Path)));
public static PackageWrapper GetWrapperFromPackage(Package package)
{
ArgumentNullException.ThrowIfNull(package);
string path;
try
{
path = IsPackageDotInstallationPathAvailable.Value ? GetInstalledPath(package) : package.InstalledLocation.Path;
}
catch (Exception e) when (e is ArgumentException || e is FileNotFoundException || e is DirectoryNotFoundException)
{
return new PackageWrapper(
package.Id.Name,
package.Id.FullName,
package.Id.FamilyName,
package.IsFramework,
package.IsDevelopmentMode,
string.Empty);
}
return new PackageWrapper(
package.Id.Name,
package.Id.FullName,
package.Id.FamilyName,
package.IsFramework,
package.IsDevelopmentMode,
path);
}
// This is a separate method so the reference to .InstalledPath won't be loaded in API versions which do not support this API (e.g. older then Build 19041)
private static string GetInstalledPath(Package package)
=> package.InstalledLocation.Path;
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Apps.Programs;
/// <summary>
/// Contains user added folder location contents as well as all user disabled applications
/// </summary>
/// <remarks>
/// <para>Win32 class applications set UniqueIdentifier using their full file path</para>
/// <para>UWP class applications set UniqueIdentifier using their Application User Model ID</para>
/// <para>Custom user added program sources set UniqueIdentifier using their location</para>
/// </remarks>
public class ProgramSource
{
public string Location { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
public string UniqueIdentifier { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using System.Xml.Linq;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public partial class UWP
{
private static readonly IPath Path = new FileSystem().Path;
private static readonly Dictionary<string, PackageVersion> _versionFromNamespace = new()
{
{ "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 },
{ "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 },
{ "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 },
};
public string Name { get; }
public string FullName { get; }
public string FamilyName { get; }
public string Location { get; set; } = string.Empty;
// Localized path based on windows display language
public string LocationLocalized { get; set; } = string.Empty;
public IList<UWPApplication> Apps { get; private set; } = new List<UWPApplication>();
public PackageVersion Version { get; set; }
public static IPackageManager PackageManagerWrapper { get; set; } = new PackageManagerWrapper();
public UWP(IPackage package)
{
ArgumentNullException.ThrowIfNull(package);
Name = package.Name;
FullName = package.FullName;
FamilyName = package.FamilyName;
}
public void InitializeAppInfo(string installedLocation)
{
Location = installedLocation;
LocationLocalized = ShellLocalization.Instance.GetLocalizedPath(installedLocation);
var path = Path.Combine(installedLocation, "AppxManifest.xml");
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
const uint noAttribute = 0x80;
var access = (uint)STGM.READ;
var hResult = PInvoke.SHCreateStreamOnFileEx(path, access, noAttribute, false, null, out IStream stream);
// S_OK
if (hResult == 0)
{
Apps = AppxPackageHelper.GetAppsFromManifest(stream).Select(appInManifest => new UWPApplication(appInManifest, this)).Where(a =>
{
var valid =
!string.IsNullOrEmpty(a.UserModelId) &&
!string.IsNullOrEmpty(a.DisplayName) &&
a.AppListEntry != "none";
return valid;
}).ToList();
}
else
{
Apps = Array.Empty<UWPApplication>();
}
}
// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
private static string[] XmlNamespaces(string path)
{
var z = XDocument.Load(path);
if (z.Root != null)
{
var namespaces = z.Root.Attributes().
Where(a => a.IsNamespaceDeclaration).
GroupBy(
a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName,
a => XNamespace.Get(a.Value)).Select(
g => g.First().ToString()).ToArray();
return namespaces;
}
else
{
return Array.Empty<string>();
}
}
private void InitPackageVersion(string[] namespaces)
{
foreach (var n in _versionFromNamespace.Keys.Where(namespaces.Contains))
{
Version = _versionFromNamespace[n];
return;
}
Version = PackageVersion.Unknown;
}
public static UWPApplication[] All()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
if (support)
{
var applications = CurrentUserPackages().AsParallel().SelectMany(p =>
{
UWP u;
try
{
u = new UWP(p);
u.InitializeAppInfo(p.InstalledLocation);
}
catch (Exception )
{
return Array.Empty<UWPApplication>();
}
return u.Apps;
});
var updatedListWithoutDisabledApps = applications
.Where(t1 => AllAppsSettings.Instance.DisabledProgramSources.All(x => x.UniqueIdentifier != t1.UniqueIdentifier))
.Select(x => x);
return updatedListWithoutDisabledApps.ToArray();
}
else
{
return Array.Empty<UWPApplication>();
}
}
private static IEnumerable<IPackage> CurrentUserPackages()
{
return PackageManagerWrapper.FindPackagesForCurrentUser().Where(p =>
{
try
{
var f = p.IsFramework;
var path = p.InstalledLocation;
return !f && !string.IsNullOrEmpty(path);
}
catch (Exception )
{
return false;
}
});
}
public override string ToString()
{
return FamilyName;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "Using CurrentCultureIgnoreCase since this is used with FamilyName")]
public override bool Equals(object? obj)
{
if (obj is UWP uwp)
{
// Using CurrentCultureIgnoreCase since this is used with FamilyName
return FamilyName.Equals(uwp.FamilyName, StringComparison.CurrentCultureIgnoreCase);
}
else
{
return false;
}
}
public override int GetHashCode()
{
// Using CurrentCultureIgnoreCase since this is used with FamilyName
return FamilyName.GetHashCode(StringComparison.CurrentCultureIgnoreCase);
}
public enum PackageVersion
{
Windows10,
Windows81,
Windows8,
Unknown,
}
}

View File

@@ -0,0 +1,608 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public class UWPApplication : IProgram
{
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private static readonly IFile File = FileSystem.File;
public string AppListEntry { get; set; } = string.Empty;
public string UniqueIdentifier { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public string UserModelId { get; set; }
public string BackgroundColor { get; set; }
public string EntryPoint { get; set; }
public string Name => DisplayName;
public string Location => Package.Location;
// Localized path based on windows display language
public string LocationLocalized => Package.LocationLocalized;
public bool Enabled { get; set; }
public bool CanRunElevated { get; set; }
public string LogoPath { get; set; } = string.Empty;
public LogoType LogoType { get; set; }
public UWP Package { get; set; }
private string logoUri;
private const string ContrastWhite = "contrast-white";
private const string ContrastBlack = "contrast-black";
// Function to set the subtitle based on the Type of application
public static string Type()
{
return Resources.packaged_application;
}
public List<CommandContextItem> GetCommands()
{
List<CommandContextItem> commands = new List<CommandContextItem>();
if (CanRunElevated)
{
commands.Add(
new CommandContextItem(
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true)));
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
}
commands.Add(
new CommandContextItem(
new OpenPathCommand(Location)
{
Name = Resources.open_containing_folder,
Icon = new("\ue838"),
}));
commands.Add(
new CommandContextItem(
new OpenInConsoleCommand(Package.Location)));
return commands;
}
public UWPApplication(IAppxManifestApplication manifestApp, UWP package)
{
ArgumentNullException.ThrowIfNull(manifestApp);
var hr = manifestApp.GetAppUserModelId(out var tmpUserModelId);
UserModelId = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUserModelId);
hr = manifestApp.GetAppUserModelId(out var tmpUniqueIdentifier);
UniqueIdentifier = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUniqueIdentifier);
hr = manifestApp.GetStringValue("DisplayName", out var tmpDisplayName);
DisplayName = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDisplayName);
hr = manifestApp.GetStringValue("Description", out var tmpDescription);
Description = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDescription);
hr = manifestApp.GetStringValue("BackgroundColor", out var tmpBackgroundColor);
BackgroundColor = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpBackgroundColor);
hr = manifestApp.GetStringValue("EntryPoint", out var tmpEntryPoint);
EntryPoint = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpEntryPoint);
Package = package ?? throw new ArgumentNullException(nameof(package));
DisplayName = ResourceFromPri(package.FullName, DisplayName);
Description = ResourceFromPri(package.FullName, Description);
logoUri = LogoUriFromManifest(manifestApp);
Enabled = true;
CanRunElevated = IfApplicationCanRunElevated();
}
private bool IfApplicationCanRunElevated()
{
if (EntryPoint == "Windows.FullTrustApplication")
{
return true;
}
else
{
var manifest = Package.Location + "\\AppxManifest.xml";
if (File.Exists(manifest))
{
try
{
// Check the manifest to verify if the Trust Level for the application is "mediumIL"
var file = File.ReadAllText(manifest);
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(file);
var xmlRoot = xmlDoc.DocumentElement;
var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
var trustLevelNode = xmlRoot?.SelectSingleNode("//*[local-name()='Application' and @uap10:TrustLevel]", namespaceManager); // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
if (trustLevelNode?.Attributes?["uap10:TrustLevel"]?.Value == "mediumIL")
{
return true;
}
}
catch (Exception)
{
}
}
}
return false;
}
internal string ResourceFromPri(string packageFullName, string resourceReference)
{
const string prefix = "ms-resource:";
// Using OrdinalIgnoreCase since this is used internally
if (!string.IsNullOrWhiteSpace(resourceReference) && resourceReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
// magic comes from @talynone
// https://github.com/talynone/Wox.Plugin.WindowsUniversalAppLauncher/blob/master/StoreAppLauncher/Helpers/NativeApiHelper.cs#L139-L153
var key = resourceReference.Substring(prefix.Length);
string parsed;
var parsedFallback = string.Empty;
// Using Ordinal/OrdinalIgnoreCase since these are used internally
if (key.StartsWith("//", StringComparison.Ordinal))
{
parsed = prefix + key;
}
else if (key.StartsWith('/'))
{
parsed = prefix + "//" + key;
}
else if (key.Contains("resources", StringComparison.OrdinalIgnoreCase))
{
parsed = prefix + key;
}
else
{
parsed = prefix + "///resources/" + key;
// e.g. for Windows Terminal version >= 1.12 DisplayName and Description resources are not in the 'resources' subtree
parsedFallback = prefix + "///" + key;
}
var outBuffer = new StringBuilder(128);
var source = $"@{{{packageFullName}? {parsed}}}";
var capacity = (uint)outBuffer.Capacity;
var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero);
if (hResult != HRESULT.S_OK)
{
if (!string.IsNullOrEmpty(parsedFallback))
{
var sourceFallback = $"@{{{packageFullName}? {parsedFallback}}}";
hResult = SHLoadIndirectString(sourceFallback, outBuffer, capacity, IntPtr.Zero);
if (hResult == HRESULT.S_OK)
{
var loaded = outBuffer.ToString();
if (!string.IsNullOrEmpty(loaded))
{
return loaded;
}
else
{
return string.Empty;
}
}
}
// https://github.com/Wox-launcher/Wox/issues/964
// known hresult 2147942522:
// 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'.
// for
// Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description
// Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription
return string.Empty;
}
else
{
var loaded = outBuffer.ToString();
if (!string.IsNullOrEmpty(loaded))
{
return loaded;
}
else
{
return string.Empty;
}
}
}
else
{
return resourceReference;
}
}
private static readonly Dictionary<PackageVersion, string> _logoKeyFromVersion = new Dictionary<PackageVersion, string>
{
{ PackageVersion.Windows10, "Square44x44Logo" },
{ PackageVersion.Windows81, "Square30x30Logo" },
{ PackageVersion.Windows8, "SmallLogo" },
};
internal string LogoUriFromManifest(IAppxManifestApplication app)
{
if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
{
var hr = app.GetStringValue(key, out var logoUriFromApp);
_ = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, logoUriFromApp);
return logoUriFromApp;
}
else
{
return string.Empty;
}
}
public void UpdateLogoPath(Theme theme)
{
LogoPathFromUri(logoUri, theme);
}
// scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
private static readonly Dictionary<PackageVersion, List<int>> _scaleFactors = new Dictionary<PackageVersion, List<int>>
{
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
{ PackageVersion.Windows8, new List<int> { 100 } },
};
private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false)
{
var extension = Path.GetExtension(path);
if (extension != null)
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { };
if (!highContrast)
{
paths.Add(path);
}
if (_scaleFactors.TryGetValue(Package.Version, out var factors))
{
foreach (var factor in factors)
{
if (highContrast)
{
paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}");
paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}");
}
else
{
paths.Add($"{prefix}.scale-{factor}{extension}");
}
}
}
var selectedIconPath = paths.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selectedIconPath))
{
LogoPath = selectedIconPath;
if (highContrast)
{
LogoType = LogoType.HighContrast;
}
else
{
LogoType = LogoType.Colored;
}
return true;
}
}
return false;
}
private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false)
{
var extension = Path.GetExtension(path);
if (extension != null)
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { };
const int appIconSize = 36;
var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 }.AsParallel();
var pathFactorPairs = new Dictionary<string, int>();
foreach (var factor in targetSizes)
{
if (highContrast)
{
var suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}";
var prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}";
paths.Add(suffixThemePath);
paths.Add(prefixThemePath);
pathFactorPairs.Add(suffixThemePath, factor);
pathFactorPairs.Add(prefixThemePath, factor);
}
else
{
var simplePath = $"{prefix}.targetsize-{factor}{extension}";
var altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}";
paths.Add(simplePath);
paths.Add(altformUnPlatedPath);
pathFactorPairs.Add(simplePath, factor);
pathFactorPairs.Add(altformUnPlatedPath, factor);
}
}
var selectedIconPath = paths.OrderBy(x => Math.Abs(pathFactorPairs.GetValueOrDefault(x) - appIconSize)).FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selectedIconPath))
{
LogoPath = selectedIconPath;
if (highContrast)
{
LogoType = LogoType.HighContrast;
}
else
{
LogoType = LogoType.Colored;
}
return true;
}
}
return false;
}
private bool SetColoredIcon(string path, string colorscheme)
{
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
if (isSetColoredScaleIcon)
{
return true;
}
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
if (isSetColoredTargetIcon)
{
return true;
}
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
if (isSetHighContrastScaleIcon)
{
return true;
}
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
if (isSetHighContrastTargetIcon)
{
return true;
}
return false;
}
private bool SetHighContrastIcon(string path, string colorscheme)
{
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
if (isSetHighContrastScaleIcon)
{
return true;
}
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
if (isSetHighContrastTargetIcon)
{
return true;
}
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
if (isSetColoredScaleIcon)
{
return true;
}
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
if (isSetColoredTargetIcon)
{
return true;
}
return false;
}
internal void LogoPathFromUri(string uri, Theme theme)
{
// all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
// windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx
// windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size
// windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx
string path;
bool isLogoUriSet;
// Using Ordinal since this is used internally with uri
if (uri.Contains('\\', StringComparison.Ordinal))
{
path = Path.Combine(Package.Location, uri);
}
else
{
// for C:\Windows\MiracastView etc
path = Path.Combine(Package.Location, "Assets", uri);
}
switch (theme)
{
case Theme.HighContrastBlack:
case Theme.HighContrastOne:
case Theme.HighContrastTwo:
isLogoUriSet = SetHighContrastIcon(path, ContrastBlack);
break;
case Theme.HighContrastWhite:
isLogoUriSet = SetHighContrastIcon(path, ContrastWhite);
break;
case Theme.Light:
isLogoUriSet = SetColoredIcon(path, ContrastWhite);
break;
default:
isLogoUriSet = SetColoredIcon(path, ContrastBlack);
break;
}
if (!isLogoUriSet)
{
LogoPath = string.Empty;
LogoType = LogoType.Error;
}
}
/*
public ImageSource Logo()
{
if (LogoType == LogoType.Colored)
{
var logo = ImageFromPath(LogoPath);
var platedImage = PlatedImage(logo);
return platedImage;
}
else
{
return ImageFromPath(LogoPath);
}
}
private const int _dpiScale100 = 96;
private ImageSource PlatedImage(BitmapImage image)
{
if (!string.IsNullOrEmpty(BackgroundColor))
{
string currentBackgroundColor;
if (BackgroundColor == "transparent")
{
// Using InvariantCulture since this is internal
currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture);
}
else
{
currentBackgroundColor = BackgroundColor;
}
var padding = 8;
var width = image.Width + (2 * padding);
var height = image.Height + (2 * padding);
var x = 0;
var y = 0;
var group = new DrawingGroup();
var converted = ColorConverter.ConvertFromString(currentBackgroundColor);
if (converted != null)
{
var color = (Color)converted;
var brush = new SolidColorBrush(color);
var pen = new Pen(brush, 1);
var backgroundArea = new Rect(0, 0, width, height);
var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8);
var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry);
group.Children.Add(rectDrawing);
var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height);
var imageDrawing = new ImageDrawing(image, imageArea);
group.Children.Add(imageDrawing);
// http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
var visual = new DrawingVisual();
var context = visual.RenderOpen();
context.DrawDrawing(group);
context.Close();
var bitmap = new RenderTargetBitmap(
Convert.ToInt32(width),
Convert.ToInt32(height),
_dpiScale100,
_dpiScale100,
PixelFormats.Pbgra32);
bitmap.Render(visual);
return bitmap;
}
else
{
ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location);
return new BitmapImage(new Uri(Constant.ErrorIcon));
}
}
else
{
// todo use windows theme as background
return image;
}
}
private BitmapImage ImageFromPath(string path)
{
if (File.Exists(path))
{
var memoryStream = new MemoryStream();
using (var fileStream = File.OpenRead(path))
{
fileStream.CopyTo(memoryStream);
memoryStream.Position = 0;
var image = new BitmapImage();
image.BeginInit();
image.StreamSource = memoryStream;
image.EndInit();
return image;
}
}
else
{
// ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path);
return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
}
}
*/
public override string ToString()
{
return $"{DisplayName}: {Description}";
}
}

View File

@@ -0,0 +1,847 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public class Win32Program : IProgram
{
public static readonly Win32Program InvalidProgram = new Win32Program { Valid = false, Enabled = false };
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private static readonly IFile File = FileSystem.File;
private static readonly IDirectory Directory = FileSystem.Directory;
public string Name { get; set; } = string.Empty;
// Localized name based on windows display language
public string NameLocalized { get; set; } = string.Empty;
public string UniqueIdentifier { get; set; } = string.Empty;
public string IcoPath { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// Path of app executable or lnk target executable
public string FullPath { get; set; } = string.Empty;
// Localized path based on windows display language
public string FullPathLocalized { get; set; } = string.Empty;
public string ParentDirectory { get; set; } = string.Empty;
public string ExecutableName { get; set; } = string.Empty;
// Localized executable name based on windows display language
public string ExecutableNameLocalized { get; set; } = string.Empty;
// Path to the lnk file on LnkProgram
public string LnkFilePath { get; set; } = string.Empty;
public string LnkResolvedExecutableName { get; set; } = string.Empty;
// Localized path based on windows display language
public string LnkResolvedExecutableNameLocalized { get; set; } = string.Empty;
public bool Valid { get; set; }
public bool Enabled { get; set; }
public bool HasArguments => !string.IsNullOrEmpty(Arguments);
public string Arguments { get; set; } = string.Empty;
public string Location => ParentDirectory;
public ApplicationType AppType { get; set; }
// Wrappers for File Operations
public static IFileVersionInfoWrapper FileVersionInfoWrapper { get; set; } = new FileVersionInfoWrapper();
public static IFile FileWrapper { get; set; } = new FileSystem().File;
private const string ShortcutExtension = "lnk";
private const string ApplicationReferenceExtension = "appref-ms";
private const string InternetShortcutExtension = "url";
private static readonly HashSet<string> ExecutableApplicationExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "cpl", "msc", "msi", "cmd", "ps1", "job", "msp", "mst", "sct", "ws", "wsh", "wsf" };
private const string ProxyWebApp = "_proxy.exe";
private const string AppIdArgument = "--app-id";
public enum ApplicationType
{
WebApplication = 0,
InternetShortcutApplication = 1,
Win32Application = 2,
ShortcutApplication = 3,
ApprefApplication = 4,
RunCommand = 5,
Folder = 6,
GenericFile = 7,
}
public bool IsWebApplication()
{
// To Filter PWAs when the user searches for the main application
// All Chromium based applications contain the --app-id argument
// Reference : https://codereview.chromium.org/399045
// Using Ordinal IgnoreCase since this is used internally
return !string.IsNullOrEmpty(FullPath) &&
!string.IsNullOrEmpty(Arguments) &&
FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) &&
Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase);
}
// Condition to Filter pinned Web Applications or PWAs when searching for the main application
public bool FilterWebApplication(string query)
{
// If the app is not a web application, then do not filter it
if (!IsWebApplication())
{
return false;
}
var subqueries = query?.Split() ?? Array.Empty<string>();
var nameContainsQuery = false;
var pathContainsQuery = false;
// check if any space separated query is a part of the app name or path name
foreach (var subquery in subqueries)
{
// Using OrdinalIgnoreCase since these are used internally
if (FullPath.Contains(subquery, StringComparison.OrdinalIgnoreCase))
{
pathContainsQuery = true;
}
if (Name.Contains(subquery, StringComparison.OrdinalIgnoreCase))
{
nameContainsQuery = true;
}
}
return pathContainsQuery && !nameContainsQuery;
}
// Function to set the subtitle based on the Type of application
public string Type()
{
switch (AppType)
{
case ApplicationType.Win32Application:
case ApplicationType.ShortcutApplication:
case ApplicationType.ApprefApplication:
return Resources.application;
case ApplicationType.InternetShortcutApplication:
return Resources.internet_shortcut_application;
case ApplicationType.WebApplication:
return Resources.web_application;
case ApplicationType.RunCommand:
return Resources.run_command;
case ApplicationType.Folder:
return Resources.folder;
case ApplicationType.GenericFile:
return Resources.file;
default:
return string.Empty;
}
}
public bool QueryEqualsNameForRunCommands(string query)
{
if (query != null && AppType == ApplicationType.RunCommand)
{
// Using OrdinalIgnoreCase since this is used internally
if (!query.Equals(Name, StringComparison.OrdinalIgnoreCase) && !query.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
public List<CommandContextItem> GetCommands()
{
List<CommandContextItem> commands = new List<CommandContextItem>();
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
{
commands.Add(new CommandContextItem(
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false)));
commands.Add(new CommandContextItem(
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory)));
}
commands.Add(new CommandContextItem(
new OpenPathCommand(ParentDirectory)));
commands.Add(new CommandContextItem(
new OpenInConsoleCommand(ParentDirectory)));
return commands;
}
public override string ToString()
{
return ExecutableName;
}
private static Win32Program CreateWin32Program(string path)
{
try
{
var parentDir = Directory.GetParent(path);
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = path,
// Using InvariantCulture since this is user facing
FullPath = path,
UniqueIdentifier = path,
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
Description = string.Empty,
Valid = true,
Enabled = true,
AppType = ApplicationType.Win32Application,
// Localized name, path and executable based on windows display language
NameLocalized = ShellLocalization.Instance.GetLocalizedName(path),
FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(path),
ExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(path)),
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
return InvalidProgram;
}
catch (Exception)
{
return InvalidProgram;
}
}
private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run|open)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled);
// This function filters Internet Shortcut programs
private static Win32Program InternetShortcutProgram(string path)
{
try
{
// We don't want to read the whole file if we don't need to
var lines = FileWrapper.ReadLines(path);
var iconPath = string.Empty;
var urlPath = string.Empty;
var validApp = false;
const string urlPrefix = "URL=";
const string iconFilePrefix = "IconFile=";
foreach (var line in lines)
{
// Using OrdinalIgnoreCase since this is used internally
if (line.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
{
urlPath = line.Substring(urlPrefix.Length);
if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out var _))
{
return InvalidProgram;
}
// To filter out only those steam shortcuts which have 'run' or 'rungameid' as the hostname
if (InternetShortcutURLPrefixes.Match(urlPath).Success)
{
validApp = true;
}
}
else if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase))
{
iconPath = line.Substring(iconFilePrefix.Length);
}
// If we resolved an urlPath & and an iconPath quit reading the file
if (!string.IsNullOrEmpty(urlPath) && !string.IsNullOrEmpty(iconPath))
{
break;
}
}
if (!validApp)
{
return InvalidProgram;
}
try
{
var parentDir = Directory.GetParent(path);
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = iconPath,
FullPath = urlPath,
UniqueIdentifier = path,
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
Valid = true,
Enabled = true,
AppType = ApplicationType.InternetShortcutApplication,
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
return InvalidProgram;
}
}
catch (Exception)
{
return InvalidProgram;
}
}
private static Win32Program LnkProgram(string path)
{
try
{
var program = CreateWin32Program(path);
var shellLinkHelper = new ShellLinkHelper();
var target = shellLinkHelper.RetrieveTargetPath(path);
if (!string.IsNullOrEmpty(target))
{
if (!(File.Exists(target) || Directory.Exists(target)))
{
// If the link points nowhere, consider it invalid.
return InvalidProgram;
}
program.LnkFilePath = program.FullPath;
program.LnkResolvedExecutableName = Path.GetFileName(target);
program.LnkResolvedExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(target));
// Using CurrentCulture since this is user facing
program.FullPath = Path.GetFullPath(target);
program.FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(target);
program.Arguments = shellLinkHelper.Arguments;
// A .lnk could be a (Chrome) PWA, set correct AppType
program.AppType = program.IsWebApplication()
? ApplicationType.WebApplication
: GetAppTypeFromPath(target);
var description = shellLinkHelper.Description;
if (!string.IsNullOrEmpty(description))
{
program.Description = description;
}
else
{
var info = FileVersionInfoWrapper.GetVersionInfo(target);
if (!string.IsNullOrEmpty(info?.FileDescription))
{
program.Description = info.FileDescription;
}
}
}
return program;
}
catch (System.IO.FileLoadException)
{
return InvalidProgram;
}
// Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
// Error caused likely due to trying to get the description of the program
catch (Exception)
{
return InvalidProgram;
}
}
private static Win32Program ExeProgram(string path)
{
try
{
var program = CreateWin32Program(path);
var info = FileVersionInfoWrapper.GetVersionInfo(path);
if (!string.IsNullOrEmpty(info?.FileDescription))
{
program.Description = info.FileDescription;
}
return program;
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
return InvalidProgram;
}
catch (FileNotFoundException)
{
return InvalidProgram;
}
catch (Exception)
{
return InvalidProgram;
}
}
// Function to get the application type, given the path to the application
public static ApplicationType GetAppTypeFromPath(string path)
{
ArgumentNullException.ThrowIfNull(path);
var extension = Extension(path);
// Using OrdinalIgnoreCase since these are used internally with paths
if (ExecutableApplicationExtensions.Contains(extension))
{
return ApplicationType.Win32Application;
}
else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.ShortcutApplication;
}
else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.ApprefApplication;
}
else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.InternetShortcutApplication;
}
else if (string.IsNullOrEmpty(extension) && System.IO.Directory.Exists(path))
{
return ApplicationType.Folder;
}
return ApplicationType.GenericFile;
}
// Function to get the Win32 application, given the path to the application
public static Win32Program? GetAppFromPath(string path)
{
ArgumentNullException.ThrowIfNull(path);
Win32Program? app;
switch (GetAppTypeFromPath(path))
{
case ApplicationType.Win32Application:
app = ExeProgram(path);
break;
case ApplicationType.ShortcutApplication:
app = LnkProgram(path);
break;
case ApplicationType.ApprefApplication:
app = CreateWin32Program(path);
app.AppType = ApplicationType.ApprefApplication;
break;
case ApplicationType.InternetShortcutApplication:
app = InternetShortcutProgram(path);
break;
case ApplicationType.WebApplication:
case ApplicationType.RunCommand:
case ApplicationType.Folder:
case ApplicationType.GenericFile:
default:
app = null;
break;
}
// if the app is valid, only then return the application, else return null
return app?.Valid == true
? app
: null;
}
private static IEnumerable<string> ProgramPaths(string directory, IList<string> suffixes, bool recursiveSearch = true)
{
if (!Directory.Exists(directory))
{
return Array.Empty<string>();
}
var files = new List<string>();
var folderQueue = new Queue<string>();
folderQueue.Enqueue(directory);
// Keep track of already visited directories to avoid cycles.
var alreadyVisited = new HashSet<string>();
do
{
var currentDirectory = folderQueue.Dequeue();
if (alreadyVisited.Contains(currentDirectory))
{
continue;
}
alreadyVisited.Add(currentDirectory);
try
{
foreach (var suffix in suffixes)
{
try
{
files.AddRange(Directory.EnumerateFiles(currentDirectory, $"*.{suffix}", SearchOption.TopDirectoryOnly));
}
catch (DirectoryNotFoundException)
{
}
}
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
}
catch (Exception)
{
}
try
{
// If the search is set to be non-recursive, then do not enqueue the child directories.
if (!recursiveSearch)
{
continue;
}
foreach (var childDirectory in Directory.EnumerateDirectories(currentDirectory, "*", new EnumerationOptions()
{
// https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions?view=net-6.0
// Exclude directories with the Reparse Point file attribute, to avoid loops due to symbolic links / directory junction / mount points.
AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint,
RecurseSubdirectories = false,
}))
{
folderQueue.Enqueue(childDirectory);
}
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
}
catch (Exception)
{
}
}
while (folderQueue.Count > 0);
return files;
}
private static string Extension(string path)
{
// Using InvariantCulture since this is user facing
var extension = Path.GetExtension(path)?.ToLowerInvariant();
return !string.IsNullOrEmpty(extension)
? extension.Substring(1)
: string.Empty;
}
private static IEnumerable<string> CustomProgramPaths(IEnumerable<ProgramSource> sources, IList<string> suffixes)
=> sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled)
.SelectMany(programSource => ProgramPaths(programSource.Location, suffixes))
.ToList() ?? Enumerable.Empty<string>();
// Function to obtain the list of applications, the locations of which have been added to the env variable PATH
private static List<string> PathEnvironmentProgramPaths(IList<string> suffixes)
{
// To get all the locations stored in the PATH env variable
var pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
var searchPaths = pathEnvVariable?.Split(Path.PathSeparator);
var toFilterAllPaths = new List<string>();
var isRecursiveSearch = true;
if (searchPaths is not null)
{
foreach (var path in searchPaths)
{
if (path.Length > 0)
{
// to expand any environment variables present in the path
var directory = Environment.ExpandEnvironmentVariables(path);
var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch);
toFilterAllPaths.AddRange(paths);
}
}
}
return toFilterAllPaths;
}
private static List<string> IndexPath(IList<string> suffixes, List<string> indexLocations)
=> indexLocations
.SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes))
.ToList();
private static List<string> StartMenuProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static List<string> DesktopProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static List<string> RegistryAppProgramPaths(IList<string> suffixes)
{
// https://msdn.microsoft.com/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
var paths = new List<string>();
using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
{
if (root != null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
{
if (root != null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
return paths
.Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)))
.Select(ExpandEnvironmentVariables)
.Where(path => path is not null)
.ToList();
}
private static IEnumerable<string> GetPathsFromRegistry(RegistryKey root)
=> root
.GetSubKeyNames()
.Select(x => GetPathFromRegistrySubkey(root, x));
private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey)
{
var path = string.Empty;
try
{
using (var key = root.OpenSubKey(subkey))
{
if (key == null)
{
return string.Empty;
}
var defaultValue = string.Empty;
path = key.GetValue(defaultValue) as string;
}
if (string.IsNullOrEmpty(path))
{
return string.Empty;
}
// fix path like this: ""\"C:\\folder\\executable.exe\""
return path = path.Trim('"', ' ');
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
return string.Empty;
}
}
private static string ExpandEnvironmentVariables(string path) =>
!string.IsNullOrEmpty(path)
? Environment.ExpandEnvironmentVariables(path)
: string.Empty;
// Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage
public override int GetHashCode()
=> Win32ProgramEqualityComparer.Default.GetHashCode(this);
public override bool Equals(object? obj)
=> obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program);
private sealed class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer();
public bool Equals(Win32Program? app1, Win32Program? app2)
{
if (app1 == null && app2 == null)
{
return true;
}
return app1 != null
&& app2 != null
&& (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant())
.Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant()));
}
public int GetHashCode(Win32Program obj)
=> (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode();
}
public static List<Win32Program> DeduplicatePrograms(IEnumerable<Win32Program> programs)
=> new HashSet<Win32Program>(programs, Win32ProgramEqualityComparer.Default).ToList();
private static Win32Program GetProgramFromPath(string path)
{
var extension = Extension(path);
if (ExecutableApplicationExtensions.Contains(extension))
{
return ExeProgram(path);
}
switch (extension)
{
case ShortcutExtension:
return LnkProgram(path);
case ApplicationReferenceExtension:
return CreateWin32Program(path);
case InternetShortcutExtension:
return InternetShortcutProgram(path);
default:
return InvalidProgram;
}
}
private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string? icoPath)
{
icoPath = null;
if (program.AppType != ApplicationType.RunCommand)
{
return false;
}
if (string.IsNullOrEmpty(program.FullPath))
{
return false;
}
// https://msdn.microsoft.com/library/windows/desktop/ee872121
try
{
var redirectionPath = ReparsePoint.GetTarget(program.FullPath);
if (string.IsNullOrEmpty(redirectionPath))
{
return false;
}
icoPath = ExpandEnvironmentVariables(redirectionPath);
return true;
}
catch (IOException)
{
}
icoPath = null;
return false;
}
private static Win32Program GetRunCommandProgramFromPath(string path)
{
var program = GetProgramFromPath(path);
if (program.Valid)
{
program.AppType = ApplicationType.RunCommand;
if (TryGetIcoPathForRunCommandProgram(program, out var icoPath))
{
program.IcoPath = icoPath ?? string.Empty;
}
}
return program;
}
public static IList<Win32Program> All(AllAppsSettings settings)
{
ArgumentNullException.ThrowIfNull(settings);
try
{
// Set an initial size to an expected size to prevent multiple hashSet resizes
const int defaultHashsetSize = 1000;
// Multiple paths could have the same programPaths and we don't want to resolve / lookup them multiple times
var paths = new HashSet<string>(defaultHashsetSize);
var runCommandPaths = new HashSet<string>(defaultHashsetSize);
// Parallelize multiple sources, and priority based on paths which most likely contain .lnks which are formatted
var sources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
{
(true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)),
(settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)),
(settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)),
(settings.EnableRegistrySource, () => RegistryAppProgramPaths(settings.ProgramSuffixes)),
};
// Run commands are always set as AppType "RunCommand"
var runCommandSources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
{
(settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.RunCommandSuffixes)),
};
var disabledProgramsList = settings.DisabledProgramSources;
// Get all paths but exclude all normal .Executables
paths.UnionWith(sources
.AsParallel()
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))
.Where(path => !ExecutableApplicationExtensions.Contains(Extension(path))));
runCommandPaths.UnionWith(runCommandSources
.AsParallel()
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)));
var programs = paths.AsParallel().Select(source => GetProgramFromPath(source));
var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source));
return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true));
}
catch (Exception)
{
return Array.Empty<Win32Program>();
}
}
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class AddBookmarkForm : FormContent
{
internal event TypedEventHandler<object, BookmarkData>? AddedCommand;
private readonly BookmarkData? _bookmark;
public AddBookmarkForm(BookmarkData? bookmark)
{
_bookmark = bookmark;
var name = _bookmark?.Name ?? string.Empty;
var url = _bookmark?.Bookmark ?? string.Empty;
TemplateJson = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"value": {{JsonSerializer.Serialize(name)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
},
{
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"value": {{JsonSerializer.Serialize(url)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_save}}",
"data": {
"name": "name",
"bookmark": "bookmark"
}
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload);
if (formInput == null)
{
return CommandResult.GoHome();
}
// get the name and url out of the values
var formName = formInput["name"] ?? string.Empty;
var formBookmark = formInput["bookmark"] ?? string.Empty;
var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}');
// Determine the type of the bookmark
string bookmarkType;
if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
bookmarkType = "web";
}
else if (File.Exists(formBookmark.ToString()))
{
bookmarkType = "file";
}
else if (Directory.Exists(formBookmark.ToString()))
{
bookmarkType = "folder";
}
else
{
// Default to web if we can't determine the type
bookmarkType = "web";
}
var updated = _bookmark ?? new BookmarkData();
updated.Name = formName.ToString();
updated.Bookmark = formBookmark.ToString();
updated.Type = bookmarkType;
AddedCommand?.Invoke(this, updated);
return CommandResult.GoHome();
}
}

View File

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

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkData
{
public string Name { get; set; } = string.Empty;
public string Bookmark { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
[JsonIgnore]
public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}');
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class BookmarkPlaceholderForm : FormContent
{
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Resources.bookmarks_required_placeholder);
private readonly List<string> _placeholderNames;
private readonly string _bookmark = string.Empty;
// TODO pass in an array of placeholders
public BookmarkPlaceholderForm(string name, string url, string type)
{
_bookmark = url;
var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
var matches = r.Matches(url);
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
var inputs = _placeholderNames.Select(p =>
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, ErrorMessage, p);
return $$"""
{
"type": "Input.Text",
"style": "text",
"id": "{{p}}",
"label": "{{p}}",
"isRequired": true,
"errorMessage": "{{errorMessage}}"
}
""";
}).ToList();
var allInputs = string.Join(",", inputs);
TemplateJson = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
""" + allInputs + $$"""
],
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_open}}",
"data": {
"placeholder": "placeholder"
}
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var target = _bookmark;
// parse the submitted JSON and then open the link
var formInput = JsonNode.Parse(payload);
var formObject = formInput?.AsObject();
if (formObject == null)
{
return CommandResult.GoHome();
}
foreach (var (key, value) in formObject)
{
var placeholderString = $"{{{key}}}";
var placeholderData = value?.ToString();
target = target.Replace(placeholderString, placeholderData);
}
try
{
var uri = UrlCommand.GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
}
return CommandResult.GoHome();
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class BookmarkPlaceholderPage : ContentPage
{
private readonly FormContent _bookmarkPlaceholder;
public override IContent[] GetContent() => [_bookmarkPlaceholder];
public BookmarkPlaceholderPage(BookmarkData data)
: this(data.Name, data.Bookmark, data.Type)
{
}
public BookmarkPlaceholderPage(string name, string url, string type)
{
Name = name;
Icon = new IconInfo(UrlCommand.IconFromUrl(url, type));
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks
{
public List<BookmarkData> Data { get; set; } = [];
private static readonly JsonSerializerOptions _jsonOptions = new()
{
IncludeFields = true,
};
public static Bookmarks ReadFromFile(string path)
{
var data = new Bookmarks();
// if the file exists, load it and append the new item
if (File.Exists(path))
{
var jsonStringReading = File.ReadAllText(path);
if (!string.IsNullOrEmpty(jsonStringReading))
{
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, _jsonOptions) ?? new Bookmarks();
}
}
return data;
}
public static void WriteToFile(string path, Bookmarks data)
{
var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}
}

View File

@@ -0,0 +1,183 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public partial class BookmarksCommandProvider : CommandProvider
{
private readonly List<CommandItem> _commands = [];
private readonly AddBookmarkPage _addNewCommand = new(null);
private Bookmarks? _bookmarks;
public static IconInfo DeleteIcon { get; private set; } = new("\uE74D"); // Delete
public static IconInfo EditIcon { get; private set; } = new("\uE70F"); // Edit
public BookmarksCommandProvider()
{
Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name;
Icon = new IconInfo("\uE718"); // Pin
_addNewCommand.AddedCommand += AddNewCommand_AddedCommand;
}
private void AddNewCommand_AddedCommand(object sender, BookmarkData args)
{
ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})");
if (_bookmarks != null)
{
_bookmarks.Data.Add(args);
}
SaveAndUpdateCommands();
}
// In the edit path, `args` was already in _bookmarks, we just updated it
private void Edit_AddedCommand(object sender, BookmarkData args)
{
ExtensionHost.LogMessage($"Edited bookmark ({args.Name},{args.Bookmark})");
SaveAndUpdateCommands();
}
private void SaveAndUpdateCommands()
{
if (_bookmarks != null)
{
var jsonPath = BookmarksCommandProvider.StateJsonPath();
Bookmarks.WriteToFile(jsonPath, _bookmarks);
}
LoadCommands();
RaiseItemsChanged(0);
}
private void LoadCommands()
{
List<CommandItem> collected = [];
collected.Add(new CommandItem(_addNewCommand));
if (_bookmarks == null)
{
LoadBookmarksFromFile();
}
if (_bookmarks != null)
{
collected.AddRange(_bookmarks.Data.Select(BookmarkToCommandItem));
}
_commands.Clear();
_commands.AddRange(collected);
}
private void LoadBookmarksFromFile()
{
try
{
var jsonFile = StateJsonPath();
if (File.Exists(jsonFile))
{
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
}
}
catch (Exception ex)
{
// debug log error
Debug.WriteLine($"Error loading commands: {ex.Message}");
}
if (_bookmarks == null)
{
_bookmarks = new();
}
}
private CommandItem BookmarkToCommandItem(BookmarkData bookmark)
{
ICommand command = bookmark.IsPlaceholder ?
new BookmarkPlaceholderPage(bookmark) :
new UrlCommand(bookmark);
var listItem = new CommandItem(command) { Icon = command.Icon };
List<CommandContextItem> contextMenu = [];
// Add commands for folder types
if (command is UrlCommand urlCommand)
{
if (urlCommand.Type == "folder")
{
contextMenu.Add(
new CommandContextItem(new DirectoryPage(urlCommand.Url)));
contextMenu.Add(
new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url)));
}
listItem.Subtitle = urlCommand.Url;
}
var edit = new AddBookmarkPage(bookmark) { Icon = EditIcon };
edit.AddedCommand += Edit_AddedCommand;
contextMenu.Add(new CommandContextItem(edit));
var delete = new CommandContextItem(
title: Resources.bookmarks_delete_title,
name: Resources.bookmarks_delete_name,
action: () =>
{
if (_bookmarks != null)
{
ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})");
_bookmarks.Data.Remove(bookmark);
SaveAndUpdateCommands();
}
},
result: CommandResult.KeepOpen())
{
IsCritical = true,
Icon = DeleteIcon,
};
contextMenu.Add(delete);
listItem.MoreCommands = contextMenu.ToArray();
return listItem;
}
public override ICommandItem[] TopLevelCommands()
{
if (_commands.Count == 0)
{
LoadCommands();
}
return _commands.ToArray();
}
internal static string StateJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return System.IO.Path.Combine(directory, "bookmarks.json");
}
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks</RootNamespace>
<Nullable>enable</Nullable>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.Bookmarks.pri</ProjectPriFileName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,45 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 17H2.66667C2.29848 17 2 16.7015 2 16.3333V1.66667C2 1.29848 2.29848 1 2.66667 1H13.3333C13.7015 1 14 1.29848 14 1.66667V16.3333C14 16.7015 13.7015 17 13.3333 17Z" fill="url(#paint0_linear_1825_17892)"/>
<g filter="url(#filter0_dd_1825_17892)">
<path d="M12.333 2.33398H3.66634C3.48224 2.33398 3.33301 2.48322 3.33301 2.66732V4.66732C3.33301 4.85141 3.48224 5.00065 3.66634 5.00065H12.333C12.5171 5.00065 12.6663 4.85141 12.6663 4.66732V2.66732C12.6663 2.48322 12.5171 2.33398 12.333 2.33398Z" fill="url(#paint1_linear_1825_17892)"/>
</g>
<path d="M5.66634 9.00065H3.66634C3.48224 9.00065 3.33301 8.85141 3.33301 8.66732V6.66732C3.33301 6.48322 3.48224 6.33398 3.66634 6.33398H5.66634C5.85044 6.33398 5.99967 6.48322 5.99967 6.66732V8.66732C5.99967 8.85141 5.85044 9.00065 5.66634 9.00065Z" fill="#CAD2D9"/>
<path d="M9.00033 9.00065H7.00033C6.81623 9.00065 6.66699 8.85141 6.66699 8.66732V6.66732C6.66699 6.48322 6.81623 6.33398 7.00033 6.33398H9.00033C9.18442 6.33398 9.33366 6.48322 9.33366 6.66732V8.66732C9.33366 8.85141 9.18443 9.00065 9.00033 9.00065Z" fill="#CAD2D9"/>
<path d="M12.3333 9.00065H10.3333C10.1492 9.00065 10 8.85141 10 8.66732V6.66732C10 6.48322 10.1492 6.33398 10.3333 6.33398H12.3333C12.5174 6.33398 12.6667 6.48322 12.6667 6.66732V8.66732C12.6667 8.85141 12.5174 9.00065 12.3333 9.00065Z" fill="#CAD2D9"/>
<path d="M5.66634 12.3327H3.66634C3.48224 12.3327 3.33301 12.1834 3.33301 11.9993V9.99935C3.33301 9.81526 3.48224 9.66602 3.66634 9.66602H5.66634C5.85044 9.66602 5.99967 9.81526 5.99967 9.99935V11.9993C5.99967 12.1834 5.85044 12.3327 5.66634 12.3327Z" fill="#CAD2D9"/>
<path d="M9.00033 12.3327H7.00033C6.81623 12.3327 6.66699 12.1834 6.66699 11.9993V9.99935C6.66699 9.81526 6.81623 9.66602 7.00033 9.66602H9.00033C9.18442 9.66602 9.33366 9.81526 9.33366 9.99935V11.9993C9.33366 12.1834 9.18443 12.3327 9.00033 12.3327Z" fill="#CAD2D9"/>
<path d="M12.3333 15.666H10.3333C10.1492 15.666 10 15.5168 10 15.3327V9.99935C10 9.81526 10.1492 9.66602 10.3333 9.66602H12.3333C12.5174 9.66602 12.6667 9.81526 12.6667 9.99935V15.3327C12.6667 15.5168 12.5174 15.666 12.3333 15.666Z" fill="url(#paint2_linear_1825_17892)"/>
<path d="M5.66634 15.6667H3.66634C3.48224 15.6667 3.33301 15.5174 3.33301 15.3333V13.3333C3.33301 13.1492 3.48224 13 3.66634 13H5.66634C5.85044 13 5.99967 13.1492 5.99967 13.3333V15.3333C5.99967 15.5174 5.85044 15.6667 5.66634 15.6667Z" fill="#CAD2D9"/>
<path d="M9.00033 15.6667H7.00033C6.81623 15.6667 6.66699 15.5174 6.66699 15.3333V13.3333C6.66699 13.1492 6.81623 13 7.00033 13H9.00033C9.18442 13 9.33366 13.1492 9.33366 13.3333V15.3333C9.33366 15.5174 9.18443 15.6667 9.00033 15.6667Z" fill="#CAD2D9"/>
<defs>
<filter id="filter0_dd_1825_17892" x="0.333008" y="0.333984" width="15.333" height="8.66602" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_17892"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_1825_17892" result="effect2_dropShadow_1825_17892"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_17892" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1825_17892" x1="12.8421" y1="17.3868" x2="3.15785" y2="0.613153" gradientUnits="userSpaceOnUse">
<stop stop-color="#626F7A"/>
<stop offset="1" stop-color="#8B9299"/>
</linearGradient>
<linearGradient id="paint1_linear_1825_17892" x1="9.68271" y1="6.58243" x2="6.31664" y2="0.752205" gradientUnits="userSpaceOnUse">
<stop stop-color="#28AFEA"/>
<stop offset="0.37387" stop-color="#3CCAF4"/>
<stop offset="0.74949" stop-color="#4BDFFC"/>
<stop offset="1" stop-color="#50E6FF"/>
</linearGradient>
<linearGradient id="paint2_linear_1825_17892" x1="12.9047" y1="15.3878" x2="9.76194" y2="9.94428" gradientUnits="userSpaceOnUse">
<stop stop-color="#173A73"/>
<stop offset="0.55519" stop-color="#134584"/>
<stop offset="1" stop-color="#114A8B"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

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

View File

@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Calc</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.Calc.pri</ProjectPriFileName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Calculator.svg" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\Calculator.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Calculator.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
public partial class ClipboardHistoryCommandsProvider : CommandProvider
{
private readonly ListItem _clipboardHistoryListItem;
public ClipboardHistoryCommandsProvider()
{
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
{
Title = "Search Clipboard History",
Icon = new IconInfo("\xE8C8"), // Copy icon
};
DisplayName = $"Clipboard History";
Icon = new IconInfo("\xE8C8"); // Copy icon
Id = "Windows.ClipboardHistory";
}
public override IListItem[] TopLevelCommands()
{
return [_clipboardHistoryListItem];
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
internal sealed partial class CopyCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
private readonly ClipboardFormat _clipboardFormat;
internal CopyCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
{
_clipboardItem = clipboardItem;
_clipboardFormat = clipboardFormat;
Name = "Copy";
if (clipboardFormat == ClipboardFormat.Text)
{
Icon = new("\xE8C8"); // Copy icon
}
else
{
Icon = new("\xE8B9"); // Picture icon
}
}
public override CommandResult Invoke()
{
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
return CommandResult.ShowToast("Copied to clipboard");
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
internal sealed partial class PasteCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
private readonly ClipboardFormat _clipboardFormat;
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
{
_clipboardItem = clipboardItem;
_clipboardFormat = clipboardFormat;
Name = "Paste";
Icon = new("\xE8C8"); // Copy icon
}
private void HideWindow()
{
// TODO GH #524: This isn't great - this requires us to have Secret Sauce in
// the clipboard extension to be able to manipulate the HWND.
// We probably need to put some window manipulation into the API, but
// what form that takes is not clear yet.
WeakReferenceMessenger.Default.Send<HideWindowMessage>(new());
}
public override CommandResult Invoke()
{
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
HideWindow();
ClipboardHelper.SendPasteKeyCombination();
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
return CommandResult.ShowToast("Pasting");
}
}

View File

@@ -0,0 +1,262 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.System;
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal static class ClipboardHelper
{
private static readonly HashSet<string> ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" };
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
[
(StandardDataFormats.Text, ClipboardFormat.Text),
(StandardDataFormats.Html, ClipboardFormat.Html),
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
internal static async Task<ClipboardFormat> GetAvailableClipboardFormatsAsync(DataPackageView clipboardData)
{
var availableClipboardFormats = DataFormats.Aggregate(
ClipboardFormat.None,
(result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result);
if (clipboardData.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await clipboardData.GetStorageItemsAsync();
if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType))
{
availableClipboardFormats |= ClipboardFormat.ImageFile;
}
}
return availableClipboardFormats;
}
internal static void SetClipboardTextContent(string text)
{
if (!string.IsNullOrEmpty(text))
{
DataPackage output = new();
output.SetText(text);
try
{
// Clipboard.SetContentWithOptions(output, null);
Clipboard.SetContent(output);
Flush();
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" });
}
catch (COMException ex)
{
ExtensionHost.LogMessage($"Error: {ex.HResult}\n{ex.Source}\n{ex.StackTrace}");
}
}
}
private static bool Flush()
{
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
// Calling inside a loop makes it work.
// Exception is: The operation is not permitted because the calling application is not the owner of the data on the clipboard.
const int maxAttempts = 5;
for (var i = 1; i <= maxAttempts; i++)
{
try
{
Task.Run(Clipboard.Flush).Wait();
return true;
}
catch (Exception ex)
{
if (i == maxAttempts)
{
ExtensionHost.LogMessage(new LogMessage()
{
Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}",
});
}
}
}
return false;
}
private static async Task<bool> FlushAsync() => await Task.Run(Flush);
internal static async Task SetClipboardFileContentAsync(string fileName)
{
var storageFile = await StorageFile.GetFileFromPathAsync(fileName);
DataPackage output = new();
output.SetStorageItems([storageFile]);
Clipboard.SetContent(output);
await FlushAsync();
}
internal static void SetClipboardImageContent(RandomAccessStreamReference image)
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied image to clipboard" });
if (image is not null)
{
DataPackage output = new();
output.SetBitmap(image);
Clipboard.SetContentWithOptions(output, null);
Flush();
}
}
internal static void SetClipboardContent(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
{
switch (clipboardFormat)
{
case ClipboardFormat.Text:
if (clipboardItem.Content == null)
{
ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" });
return;
}
else
{
SetClipboardTextContent(clipboardItem.Content);
}
break;
case ClipboardFormat.Image:
if (clipboardItem.ImageData == null)
{
ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" });
return;
}
else
{
SetClipboardImageContent(clipboardItem.ImageData);
}
break;
default:
ExtensionHost.LogMessage(new LogMessage { Message = "Unsupported clipboard format." });
break;
}
}
// Function to send a single key event
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
var ignoreKeyEventFlag = (UIntPtr)0x5555;
var inputShift = new NativeMethods.INPUT
{
type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD,
data = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = keyCode,
dwFlags = keyStatus,
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
dwExtraInfo = ignoreKeyEventFlag,
},
},
};
var inputs = new NativeMethods.INPUT[] { inputShift };
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
}
internal static void SendPasteKeyCombination()
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Sending paste keys..." });
SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp);
// Send Ctrl + V
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp);
ExtensionHost.LogMessage(new LogMessage() { Message = "Paste sent" });
}
internal static async Task<string> GetClipboardTextOrHtmlTextAsync(DataPackageView clipboardData)
{
if (clipboardData.Contains(StandardDataFormats.Text))
{
return await clipboardData.GetTextAsync();
}
else if (clipboardData.Contains(StandardDataFormats.Html))
{
var html = await clipboardData.GetHtmlFormatAsync();
return HtmlUtilities.ConvertToText(html);
}
else
{
return string.Empty;
}
}
internal static async Task<string> GetClipboardHtmlContentAsync(DataPackageView clipboardData) =>
clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty;
internal static async Task<SoftwareBitmap?> GetClipboardImageContentAsync(DataPackageView clipboardData)
{
using var stream = await GetClipboardImageStreamAsync(clipboardData);
if (stream != null)
{
var decoder = await BitmapDecoder.CreateAsync(stream);
return await decoder.GetSoftwareBitmapAsync();
}
return null;
}
private static async Task<IRandomAccessStream?> GetClipboardImageStreamAsync(DataPackageView clipboardData)
{
if (clipboardData.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await clipboardData.GetStorageItemsAsync();
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
return await file.OpenReadAsync();
}
}
if (clipboardData.Contains(StandardDataFormats.Bitmap))
{
var bitmap = await clipboardData.GetBitmapAsync();
return await bitmap.OpenReadAsync();
}
return null;
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct INPUT
{
internal INPUTTYPE type;
internal InputUnion data;
internal static int Size => Marshal.SizeOf(typeof(INPUT));
}
[StructLayout(LayoutKind.Explicit)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal uint dwFlags;
internal uint time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct KEYBDINPUT
{
internal short wVk;
internal short wScan;
internal uint dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct HARDWAREINPUT
{
internal int uMsg;
internal short wParamL;
internal short wParamH;
}
internal enum INPUTTYPE : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2,
}
[Flags]
internal enum KeyEventF
{
KeyDown = 0x0000,
ExtendedKey = 0x0001,
KeyUp = 0x0002,
Unicode = 0x0004,
Scancode = 0x0008,
}
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern short GetAsyncKeyState(int vKey);
[StructLayout(LayoutKind.Sequential)]
internal struct PointInter
{
public int X;
public int Y;
public static explicit operator Point(PointInter point) => new(point.X, point.Y);
}
[DllImport("user32.dll")]
internal static extern bool GetCursorPos(out PointInter lpPoint);
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
[Flags]
public enum ClipboardFormat
{
None,
Text = 1 << 0,
Html = 1 << 1,
Audio = 1 << 2,
Image = 1 << 3,
ImageFile = 1 << 4,
}

View File

@@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
public class ClipboardItem
{
public string? Content { get; set; }
public required ClipboardHistoryItem Item { get; set; }
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
public RandomAccessStreamReference? ImageData { get; set; }
public string GetDataType()
{
// Check if there is valid image data
if (IsImage)
{
return "Image";
}
// Check if there is valid text content
return IsText ? "Text" : "Unknown";
}
[MemberNotNullWhen(true, nameof(ImageData))]
private bool IsImage => ImageData != null;
[MemberNotNullWhen(true, nameof(Content))]
private bool IsText => !string.IsNullOrEmpty(Content);
public static List<string> ShiftLinesLeft(List<string> lines)
{
// Determine the minimum leading whitespace
var minLeadingWhitespace = lines
.Where(line => !string.IsNullOrWhiteSpace(line))
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
// Check if all lines have at least that much leading whitespace
if (lines.Any(line => line.TakeWhile(char.IsWhiteSpace).Count() < minLeadingWhitespace))
{
return lines; // Return the original lines if any line doesn't have enough leading whitespace
}
// Remove the minimum leading whitespace from each line
var shiftedLines = lines.Select(line => line.Substring(minLeadingWhitespace)).ToList();
return shiftedLines;
}
public static List<string> StripLeadingWhitespace(List<string> lines)
{
// Determine the minimum leading whitespace
var minLeadingWhitespace = lines
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
// Remove the minimum leading whitespace from each line
var shiftedLines = lines.Select(line =>
line.Length >= minLeadingWhitespace
? line.Substring(minLeadingWhitespace)
: line).ToList();
return shiftedLines;
}
public ListItem ToListItem()
{
ListItem listItem;
List<DetailsElement> metadata = [];
metadata.Add(new DetailsElement()
{
Key = "Copied on",
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
});
if (IsImage)
{
var iconData = new IconData(ImageData);
var heroImage = new IconInfo(iconData, iconData);
listItem = new(new CopyCommand(this, ClipboardFormat.Image))
{
// Placeholder subtitle as theres no BitmapImage dimensions to retrieve
Title = "Image Data",
Details = new Details()
{
HeroImage = heroImage,
Title = GetDataType(),
Body = Timestamp.ToString(CultureInfo.InvariantCulture),
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image))
],
};
}
else if (IsText)
{
var splitContent = Content.Split("\n");
var head = splitContent.AsSpan(0, Math.Min(3, splitContent.Length)).ToArray().ToList();
var preview2 = string.Join(
"\n",
StripLeadingWhitespace(head));
listItem = new(new CopyCommand(this, ClipboardFormat.Text))
{
Title = preview2,
Details = new Details
{
Title = GetDataType(),
Body = $"```text\n{Content}\n```",
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)),
],
};
}
else
{
listItem = new(new NoOpCommand())
{
Title = "Unknown",
Subtitle = GetDataType(),
Details = new Details { Title = GetDataType() },
};
}
return listItem;
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
internal sealed partial class ClipboardHistoryListPage : ListPage
{
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
private readonly string _defaultIconPath;
public ClipboardHistoryListPage()
{
clipboardHistory = [];
_defaultIconPath = string.Empty;
Icon = new("\uF0E3"); // ClipboardList icon
Name = "Clipboard History";
Id = "com.microsoft.cmdpal.clipboardHistory";
ShowDetails = true;
Clipboard.HistoryChanged += TrackClipboardHistoryChanged_EventHandler;
}
private void TrackClipboardHistoryChanged_EventHandler(object? sender, ClipboardHistoryChangedEventArgs? e) => RaiseItemsChanged(0);
private bool IsClipboardHistoryEnabled()
{
var registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
try
{
var enableClipboardHistory = (int)(Registry.GetValue(registryKey, "EnableClipboardHistory", false) ?? 0);
return enableClipboardHistory != 0;
}
catch (Exception)
{
return false;
}
}
private bool IsClipboardHistoryDisabledByGPO()
{
var registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\";
try
{
var allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null);
return allowClipboardHistory != null ? (int)allowClipboardHistory == 0 : false;
}
catch (Exception)
{
return false;
}
}
private async Task LoadClipboardHistoryAsync()
{
try
{
List<ClipboardItem> items = [];
if (!Clipboard.IsHistoryEnabled())
{
return;
}
var historyItems = await Clipboard.GetHistoryItemsAsync();
if (historyItems.Status != ClipboardHistoryItemsResultStatus.Success)
{
return;
}
foreach (var item in historyItems.Items)
{
if (item.Content.Contains(StandardDataFormats.Text))
{
var text = await item.Content.GetTextAsync();
items.Add(new ClipboardItem { Content = text, Item = item });
}
else if (item.Content.Contains(StandardDataFormats.Bitmap))
{
items.Add(new ClipboardItem { Item = item });
}
}
clipboardHistory.Clear();
foreach (var item in items)
{
if (item.Item.Content.Contains(StandardDataFormats.Bitmap))
{
var imageReceived = await item.Item.Content.GetBitmapAsync();
if (imageReceived != null)
{
item.ImageData = imageReceived;
}
}
clipboardHistory.Add(item);
}
}
catch (Exception ex)
{
// TODO GH #108 We need to figure out some logging
// Logger.LogError("Loading clipboard history failed", ex);
ExtensionHost.ShowStatus(new StatusMessage() { Message = "Loading clipboard history failed", State = MessageState.Error }, StatusContext.Page);
ExtensionHost.LogMessage(ex.ToString());
}
}
private void LoadClipboardHistoryInSTA()
{
// https://github.com/microsoft/windows-rs/issues/317
// Clipboard API needs to be called in STA or it
// hangs.
var thread = new Thread(() =>
{
var t = LoadClipboardHistoryAsync();
t.ConfigureAwait(false);
t.Wait();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
private ListItem[] GetClipboardHistoryListItems()
{
LoadClipboardHistoryInSTA();
List<ListItem> listItems = [];
for (var i = 0; i < clipboardHistory.Count; i++)
{
var item = clipboardHistory[i];
if (item != null)
{
listItems.Add(item.ToListItem());
}
}
return listItems.ToArray();
}
public override IListItem[] GetItems() => GetClipboardHistoryListItems();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,93 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1825_18138)">
<path d="M8.3125 3L7.47871 1.89974C7.26684 1.62016 6.9931 1.39344 6.67896 1.23734C6.36481 1.08125 6.01879 1.00001 5.668 1H1C0.734784 1 0.48043 1.10536 0.292893 1.29289C0.105357 1.48043 0 1.73478 0 2L0 12C0.00155124 12.53 0.212763 13.0378 0.5875 13.4125C0.962237 13.7872 1.47004 13.9984 2 14H14C14.53 13.9984 15.0378 13.7872 15.4125 13.4125C15.7872 13.0378 15.9984 12.53 16 12V4C16 3.73478 15.8946 3.48043 15.7071 3.29289C15.5196 3.10536 15.2652 3 15 3H8.3125Z" fill="url(#paint0_linear_1825_18138)"/>
<mask id="mask0_1825_18138" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="1" width="16" height="13">
<path d="M8.3125 3L7.47871 1.89974C7.26684 1.62016 6.9931 1.39344 6.67896 1.23734C6.36481 1.08125 6.01879 1.00001 5.668 1H1C0.734784 1 0.48043 1.10536 0.292893 1.29289C0.105357 1.48043 0 1.73478 0 2L0 12C0.00155124 12.53 0.212763 13.0378 0.5875 13.4125C0.962237 13.7872 1.47004 13.9984 2 14H14C14.53 13.9984 15.0378 13.7872 15.4125 13.4125C15.7872 13.0378 15.9984 12.53 16 12V4C16 3.73478 15.8946 3.48043 15.7071 3.29289C15.5196 3.10536 15.2652 3 15 3H8.3125Z" fill="url(#paint1_linear_1825_18138)"/>
</mask>
<g mask="url(#mask0_1825_18138)">
<g filter="url(#filter0_dd_1825_18138)">
<path d="M14.9985 5.00187H10L8 5.00172C7.62917 5.19449 6.41839 4.9973 6.00047 5.00171H1.00151C0.868112 4.99367 0.734525 5.01401 0.609573 5.06142C0.484622 5.10882 0.37115 5.18219 0.276655 5.27669C0.18216 5.37119 0.108792 5.48467 0.061398 5.60962C0.0140044 5.73458 -0.00633622 5.86817 0.00172002 6.00156V13.9986C-0.00634109 14.132 0.0139961 14.2656 0.0613881 14.3905C0.10878 14.5155 0.182148 14.6289 0.276644 14.7234C0.37114 14.8179 0.484614 14.8913 0.609568 14.9387C0.734521 14.9861 0.868111 15.0065 1.00151 14.9984H14.9985C15.1319 15.0065 15.2655 14.9861 15.3904 14.9387C15.5154 14.8913 15.6289 14.8179 15.7234 14.7234C15.8178 14.6289 15.8912 14.5155 15.9386 14.3905C15.986 14.2655 16.0064 14.132 15.9983 13.9986V6.00171C16.0064 5.86832 15.986 5.73473 15.9386 5.60977C15.8912 5.48481 15.8179 5.37134 15.7234 5.27683C15.6289 5.18233 15.5154 5.10896 15.3905 5.06156C15.2655 5.01416 15.1319 4.99382 14.9985 5.00187Z" fill="url(#paint2_linear_1825_18138)"/>
</g>
</g>
<path d="M14.9985 4.00172H8.2L7.20022 4.70161C6.82939 4.89438 6.41839 4.99714 6.00047 5.00156H1.00151C0.868112 4.99351 0.734525 5.01386 0.609573 5.06126C0.484622 5.10866 0.37115 5.18203 0.276655 5.27654C0.18216 5.37104 0.108792 5.48451 0.061398 5.60947C0.0140044 5.73442 -0.00633622 5.86801 0.00172002 6.00141V13.9984C-0.00634109 14.1318 0.0139961 14.2654 0.0613881 14.3904C0.10878 14.5153 0.182148 14.6288 0.276644 14.7233C0.37114 14.8178 0.484614 14.8912 0.609568 14.9386C0.734521 14.986 0.868111 15.0063 1.00151 14.9983H14.9985C15.1319 15.0063 15.2655 14.9859 15.3904 14.9385C15.5154 14.8911 15.6289 14.8178 15.7234 14.7233C15.8178 14.6288 15.8912 14.5153 15.9386 14.3903C15.986 14.2654 16.0064 14.1318 15.9983 13.9984V5.00156C16.0064 4.86816 15.986 4.73457 15.9386 4.60961C15.8912 4.48466 15.8179 4.37118 15.7234 4.27668C15.6289 4.18218 15.5154 4.1088 15.3905 4.0614C15.2655 4.01401 15.1319 3.99366 14.9985 4.00172H14.9985Z" fill="url(#paint3_linear_1825_18138)"/>
<path opacity="0.25" d="M15.7017 4.30036C15.6137 4.20263 15.5056 4.1252 15.3847 4.0734C15.2638 4.0216 15.1332 3.99668 15.0017 4.00036H8.20172L7.20172 4.70036C6.83081 4.89316 6.41972 4.99594 6.00172 5.00036H1.00172C0.868297 4.9923 0.734682 5.01264 0.609705 5.06005C0.484727 5.10745 0.371231 5.18083 0.276715 5.27535C0.182198 5.36987 0.108814 5.48336 0.0614098 5.60834C0.0140056 5.73332 -0.00633928 5.86693 0.00171929 6.00036V7.00036C-0.00634091 6.86693 0.0140029 6.73332 0.0614065 6.60834C0.10881 6.48336 0.182194 6.36986 0.276711 6.27535C0.371227 6.18083 0.484724 6.10745 0.609703 6.06004C0.734681 6.01264 0.868296 5.9923 1.00172 6.00036H6.00172C6.60283 5.98827 7.18978 5.81563 7.70172 5.50036L8.50172 5.00036H15.0017C15.1351 4.9923 15.2688 5.01265 15.3937 5.06006C15.5187 5.10746 15.6322 5.18085 15.7267 5.27536C15.8212 5.36988 15.8946 5.48337 15.942 5.60835C15.9894 5.73332 16.0098 5.86693 16.0017 6.00036V5.00036C16.0054 4.86891 15.9805 4.73823 15.9287 4.61736C15.8769 4.49649 15.7995 4.38833 15.7017 4.30036Z" fill="url(#paint4_linear_1825_18138)"/>
<mask id="mask1_1825_18138" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="16" height="11">
<path d="M14.9985 4.00172H8.2L7.20022 4.70161C6.82939 4.89438 6.41839 4.99714 6.00047 5.00156H1.00151C0.868112 4.99351 0.734525 5.01386 0.609573 5.06126C0.484622 5.10866 0.37115 5.18203 0.276655 5.27654C0.18216 5.37104 0.108792 5.48451 0.061398 5.60947C0.0140044 5.73442 -0.00633622 5.86801 0.00172002 6.00141V13.9984C-0.00634109 14.1318 0.0139961 14.2654 0.0613881 14.3904C0.10878 14.5153 0.182148 14.6288 0.276644 14.7233C0.37114 14.8178 0.484614 14.8912 0.609568 14.9386C0.734521 14.986 0.868111 15.0063 1.00151 14.9983H14.9985C15.1319 15.0063 15.2655 14.9859 15.3904 14.9385C15.5154 14.8911 15.6289 14.8178 15.7234 14.7233C15.8178 14.6288 15.8912 14.5153 15.9386 14.3903C15.986 14.2654 16.0064 14.1318 15.9983 13.9984V5.00156C16.0064 4.86816 15.986 4.73457 15.9386 4.60961C15.8912 4.48466 15.8179 4.37118 15.7234 4.27668C15.6289 4.18218 15.5154 4.1088 15.3905 4.0614C15.2655 4.01401 15.1319 3.99366 14.9985 4.00172H14.9985Z" fill="url(#paint5_linear_1825_18138)"/>
</mask>
<g mask="url(#mask1_1825_18138)">
<g filter="url(#filter1_dd_1825_18138)">
<path d="M5 10H11C11.5304 10 12.0391 10.2107 12.4142 10.5858C12.7893 10.9609 13 11.4696 13 12V15H3V12C3 11.4696 3.21071 10.9609 3.58579 10.5858C3.96086 10.2107 4.46957 10 5 10Z" fill="url(#paint6_linear_1825_18138)"/>
</g>
</g>
<path d="M5 10H11C11.5304 10 12.0391 10.2107 12.4142 10.5858C12.7893 10.9609 13 11.4696 13 12V15H3V12C3 11.4696 3.21071 10.9609 3.58579 10.5858C3.96086 10.2107 4.46957 10 5 10Z" fill="url(#paint7_linear_1825_18138)"/>
<path d="M10.5 12H5.5C5.22386 12 5 12.2239 5 12.5C5 12.7761 5.22386 13 5.5 13H10.5C10.7761 13 11 12.7761 11 12.5C11 12.2239 10.7761 12 10.5 12Z" fill="#114A8B"/>
</g>
<defs>
<filter id="filter0_dd_1825_18138" x="-1" y="4" width="18" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.166667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_18138"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_1825_18138" result="effect2_dropShadow_1825_18138"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_18138" result="shape"/>
</filter>
<filter id="filter1_dd_1825_18138" x="2" y="9" width="12" height="7" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.166667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_18138"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_1825_18138" result="effect2_dropShadow_1825_18138"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_18138" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1825_18138" x1="11.4683" y1="14.139" x2="4.1515" y2="1.4659" gradientUnits="userSpaceOnUse">
<stop offset="0.1135" stop-color="#D18B00"/>
<stop offset="0.6162" stop-color="#E09F00"/>
</linearGradient>
<linearGradient id="paint1_linear_1825_18138" x1="11.4683" y1="14.139" x2="4.1515" y2="1.4659" gradientUnits="userSpaceOnUse">
<stop offset="0.1135" stop-color="#D18B00"/>
<stop offset="0.6162" stop-color="#E09F00"/>
</linearGradient>
<linearGradient id="paint2_linear_1825_18138" x1="12.1767" y1="16.7345" x2="4.48067" y2="3.40451" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5B300"/>
<stop offset="0.5" stop-color="#FFCB3C"/>
<stop offset="1" stop-color="#FFD762"/>
</linearGradient>
<linearGradient id="paint3_linear_1825_18138" x1="12.1767" y1="16.7343" x2="4.48067" y2="3.40435" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5B300"/>
<stop offset="0.5" stop-color="#FFCB3C"/>
<stop offset="1" stop-color="#FFD762"/>
</linearGradient>
<linearGradient id="paint4_linear_1825_18138" x1="0.430309" y1="5.50017" x2="16.0017" y2="5.50017" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint5_linear_1825_18138" x1="12.1767" y1="16.7343" x2="4.48067" y2="3.40435" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5B300"/>
<stop offset="0.5" stop-color="#FFCB3C"/>
<stop offset="1" stop-color="#FFD762"/>
</linearGradient>
<linearGradient id="paint6_linear_1825_18138" x1="8.30516" y1="15.1422" x2="7.36003" y2="9.7821" gradientUnits="userSpaceOnUse">
<stop stop-color="#0062B4"/>
<stop offset="1" stop-color="#1493DF"/>
</linearGradient>
<linearGradient id="paint7_linear_1825_18138" x1="8.30516" y1="15.1422" x2="7.36003" y2="9.7821" gradientUnits="userSpaceOnUse">
<stop stop-color="#0062B4"/>
<stop offset="1" stop-color="#1493DF"/>
</linearGradient>
<clipPath id="clip0_1825_18138">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class CopyPathCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal CopyPathCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_CopyPath;
this.Icon = new IconInfo("\uE8c8");
}
public override CommandResult Invoke()
{
try
{
var dataPackage = new DataPackage();
dataPackage.SetText(_item.FullPath);
Clipboard.SetContent(dataPackage);
Clipboard.Flush();
}
catch
{
}
return CommandResult.KeepOpen();
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenFileCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal OpenFileCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenFile;
this.Icon = Icons.OpenFile;
}
public override CommandResult Invoke()
{
using (var process = new Process())
{
process.StartInfo.FileName = _item.FullPath;
process.StartInfo.UseShellExecute = true;
try
{
process.Start();
}
catch (Win32Exception ex)
{
Logger.LogError($"Unable to open {_item.FullPath}", ex);
}
}
return CommandResult.GoHome();
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenInConsoleCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal OpenInConsoleCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenPathInConsole;
this.Icon = new IconInfo("\uE756");
}
public override CommandResult Invoke()
{
using (var process = new Process())
{
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath);
process.StartInfo.FileName = "cmd.exe";
try
{
process.Start();
}
catch (Win32Exception ex)
{
Logger.LogError($"Unable to open {_item.FullPath}", ex);
}
}
return CommandResult.GoHome();
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Native;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenPropertiesCommand : InvokableCommand
{
private readonly IndexerItem _item;
private static unsafe bool ShowFileProperties(string filename)
{
var propertiesPtr = Marshal.StringToHGlobalUni("properties");
var filenamePtr = Marshal.StringToHGlobalUni(filename);
try
{
var filenamePCWSTR = new PCWSTR((char*)filenamePtr);
var propertiesPCWSTR = new PCWSTR((char*)propertiesPtr);
var info = new SHELLEXECUTEINFOW
{
cbSize = (uint)Marshal.SizeOf<SHELLEXECUTEINFOW>(),
lpVerb = propertiesPCWSTR,
lpFile = filenamePCWSTR,
nShow = (int)SHOW_WINDOW_CMD.SW_SHOW,
fMask = NativeHelpers.SEEMASKINVOKEIDLIST,
};
return PInvoke.ShellExecuteEx(ref info);
}
finally
{
Marshal.FreeHGlobal(filenamePtr);
Marshal.FreeHGlobal(propertiesPtr);
}
}
internal OpenPropertiesCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenProperties;
this.Icon = new IconInfo("\uE90F");
}
public override CommandResult Invoke()
{
try
{
ShowFileProperties(_item.FullPath);
}
catch (Exception ex)
{
Logger.LogError("Error showing file properties: ", ex);
}
return CommandResult.GoHome();
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Native;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenWithCommand : InvokableCommand
{
private readonly IndexerItem _item;
private static unsafe bool OpenWith(string filename)
{
var filenamePtr = Marshal.StringToHGlobalUni(filename);
var verbPtr = Marshal.StringToHGlobalUni("openas");
try
{
var filenamePCWSTR = new PCWSTR((char*)filenamePtr);
var verbPCWSTR = new PCWSTR((char*)verbPtr);
var info = new SHELLEXECUTEINFOW
{
cbSize = (uint)Marshal.SizeOf<SHELLEXECUTEINFOW>(),
lpVerb = verbPCWSTR,
lpFile = filenamePCWSTR,
nShow = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL,
fMask = NativeHelpers.SEEMASKINVOKEIDLIST,
};
return PInvoke.ShellExecuteEx(ref info);
}
finally
{
Marshal.FreeHGlobal(filenamePtr);
Marshal.FreeHGlobal(verbPtr);
}
}
internal OpenWithCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenWith;
this.Icon = new IconInfo("\uE7AC");
}
public override CommandResult Invoke()
{
OpenWith(_item.FullPath);
return CommandResult.GoHome();
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
namespace Microsoft.CmdPal.Ext.Indexer.Data;
internal sealed class IndexerItem
{
internal string FullPath { get; init; }
internal string FileName { get; init; }
internal bool IsDirectory()
{
if (!Path.Exists(FullPath))
{
return false;
}
var attr = File.GetAttributes(FullPath);
// detect whether it is a directory or file
return (attr & FileAttributes.Directory) == FileAttributes.Directory;
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Data;
internal sealed partial class IndexerListItem : ListItem
{
internal string FilePath { get; private set; }
public IndexerListItem(
IndexerItem indexerItem,
IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include)
: base(new OpenFileCommand(indexerItem))
{
FilePath = indexerItem.FullPath;
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
List<CommandContextItem> context = [];
if (indexerItem.IsDirectory())
{
var directoryPage = new DirectoryPage(indexerItem.FullPath);
if (browseByDefault == IncludeBrowseCommand.AsDefault)
{
// Swap the open file command into the context menu
context.Add(new CommandContextItem(Command));
Command = directoryPage;
}
else if (browseByDefault == IncludeBrowseCommand.Include)
{
context.Add(new CommandContextItem(directoryPage));
}
}
MoreCommands = [
..context,
new CommandContextItem(new OpenWithCommand(indexerItem)),
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
new CommandContextItem(new CopyPathCommand(indexerItem)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
];
}
}
internal enum IncludeBrowseCommand
{
AsDefault = 0,
Include = 1,
Exclude = 2,
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Windows.Win32;
using Windows.Win32.System.Com;
using Windows.Win32.System.Search;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
internal static class DataSourceManager
{
private static readonly Guid CLSIDCollatorDataSource = new("9E175B8B-F52A-11D8-B9A5-505054503030");
private static IDBInitialize _dataSource;
public static IDBInitialize GetDataSource()
{
if (_dataSource == null)
{
InitializeDataSource();
}
return _dataSource;
}
private static bool InitializeDataSource()
{
var hr = PInvoke.CoCreateInstance(CLSIDCollatorDataSource, null, CLSCTX.CLSCTX_INPROC_SERVER, typeof(IDBInitialize).GUID, out var dataSourceObj);
if (hr != 0)
{
Logger.LogError("CoCreateInstance failed: " + hr);
return false;
}
if (dataSourceObj == null)
{
Logger.LogError("CoCreateInstance failed: dataSourceObj is null");
return false;
}
_dataSource = (IDBInitialize)dataSourceObj;
_dataSource.Initialize();
return true;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using Windows.Win32.Storage.IndexServer;
using Windows.Win32.System.Com.StructuredStorage;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
[StructLayout(LayoutKind.Sequential)]
internal struct DBPROP
{
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
public uint dwPropertyID;
public uint dwOptions;
public uint dwStatus;
public DBID colid;
public PROPVARIANT vValue;
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
}

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