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

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
}

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;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
[StructLayout(LayoutKind.Sequential)]
public struct DBPROPIDSET
{
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
public IntPtr rgPropertyIDs; // Pointer to array of property IDs
public uint cPropertyIDs; // Number of properties in array
public Guid guidPropertySet; // GUID of the property set
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
}

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;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
[StructLayout(LayoutKind.Sequential)]
public struct DBPROPSET
{
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
public IntPtr rgProperties; // Pointer to an array of DBPROP
public uint cProperties; // Number of properties in the array
public Guid guidPropertySet; // GUID of the property set
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
[ComImport]
[Guid("0c733a7c-2a1c-11ce-ade5-00aa0044773d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IRowset
{
[PreserveSig]
int AddRefRows(
uint cRows,
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows,
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts,
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus);
[PreserveSig]
int GetData(
IntPtr hRow,
IntPtr hAccessor,
IntPtr pData);
[PreserveSig]
int GetNextRows(
IntPtr hReserved,
long lRowsOffset,
long cRows,
out uint pcRowsObtained,
out IntPtr prghRows);
[PreserveSig]
int ReleaseRows(
uint cRows,
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows,
IntPtr rgRowOptions,
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts,
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus);
}

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 System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
[ComImport]
[Guid("0C733A55-2A1C-11CE-ADE5-00AA0044773D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IRowsetInfo
{
[PreserveSig]
int GetProperties(
uint cPropertyIDSets,
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] DBPROPIDSET[] rgPropertyIDSets,
out ulong pcPropertySets,
out IntPtr prgPropertySets);
[PreserveSig]
int GetReferencedRowset(
uint iOrdinal,
[In] ref Guid riid,
[Out, MarshalAs(UnmanagedType.Interface)] out object ppReferencedRowset);
[PreserveSig]
int GetSpecification(
[In] ref Guid riid,
[Out, MarshalAs(UnmanagedType.Interface)] out object ppSpecification);
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
using Microsoft.CmdPal.Ext.Indexer.Native;
using Windows.Win32.System.Com;
using Windows.Win32.System.Com.StructuredStorage;
using Windows.Win32.UI.Shell.PropertiesSystem;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
internal sealed class SearchResult
{
public string ItemDisplayName { get; init; }
public string ItemUrl { get; init; }
public string LaunchUri { get; init; }
public bool IsFolder { get; init; }
public SearchResult(string name, string url, string filePath, bool isFolder)
{
ItemDisplayName = name;
ItemUrl = url;
IsFolder = isFolder;
if (LaunchUri == null || LaunchUri.Length == 0)
{
// Launch the file with the default app, so use the file path
LaunchUri = filePath;
}
}
public static unsafe SearchResult Create(IPropertyStore propStore)
{
try
{
var key = NativeHelpers.PropertyKeys.PKEYItemNameDisplay;
propStore.GetValue(&key, out var itemNameDisplay);
key = NativeHelpers.PropertyKeys.PKEYItemUrl;
propStore.GetValue(&key, out var itemUrl);
key = NativeHelpers.PropertyKeys.PKEYKindText;
propStore.GetValue(&key, out var kindText);
var filePath = GetFilePath(ref itemUrl);
var isFolder = IsFoder(ref kindText);
// Create the actual result object
var searchResult = new SearchResult(
GetStringFromPropVariant(ref itemNameDisplay),
GetStringFromPropVariant(ref itemUrl),
filePath,
isFolder);
return searchResult;
}
catch (Exception ex)
{
Logger.LogError("Failed to get property values from propStore.", ex);
return null;
}
}
private static bool IsFoder(ref PROPVARIANT kindText)
{
var kindString = GetStringFromPropVariant(ref kindText);
return string.Equals(kindString, "Folder", StringComparison.OrdinalIgnoreCase);
}
private static string GetFilePath(ref PROPVARIANT itemUrl)
{
var filePath = GetStringFromPropVariant(ref itemUrl);
filePath = UrlToFilePathConverter.Convert(filePath);
return filePath;
}
private static string GetStringFromPropVariant(ref PROPVARIANT propVariant)
{
if (propVariant.Anonymous.Anonymous.vt == VARENUM.VT_LPWSTR)
{
var pwszVal = propVariant.Anonymous.Anonymous.Anonymous.pwszVal;
if (pwszVal != null)
{
return pwszVal.ToString();
}
}
return string.Empty;
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
internal sealed class QueryStringBuilder
{
private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText";
private const string SystemIndex = "SystemIndex";
private const string ScopeFileConditions = "SCOPE='file:'";
private const string OrderConditions = "System.DateModified DESC";
private const string SelectQueryWithScope = "SELECT " + Properties + " FROM " + SystemIndex + " WHERE (" + ScopeFileConditions + ")";
private const string SelectQueryWithScopeAndOrderConditions = SelectQueryWithScope + " ORDER BY " + OrderConditions;
private static ISearchQueryHelper queryHelper;
public static string GeneratePrimingQuery() => SelectQueryWithScopeAndOrderConditions;
public static string GenerateQuery(string searchText, uint whereId)
{
if (queryHelper == null)
{
var searchManager = new CSearchManager();
ISearchCatalogManager catalogManager = searchManager.GetCatalog(SystemIndex);
queryHelper = catalogManager.GetQueryHelper();
queryHelper.QuerySelectColumns = Properties;
queryHelper.QueryContentProperties = "System.FileName";
queryHelper.QuerySorting = OrderConditions;
}
queryHelper.QueryWhereRestrictions = "AND " + ScopeFileConditions + "AND ReuseWhere(" + whereId.ToString(CultureInfo.InvariantCulture) + ")";
return queryHelper.GenerateSQLFromUserQuery(searchText);
}
}

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 System;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
public class UrlToFilePathConverter
{
public static string Convert(string url)
{
var result = url.Replace('/', '\\'); // replace all '/' to '\'
var fileProtocolString = "file:";
var indexProtocolFound = url.IndexOf(fileProtocolString, StringComparison.CurrentCultureIgnoreCase);
if (indexProtocolFound != -1 && (indexProtocolFound + fileProtocolString.Length) < url.Length)
{
result = result[(indexProtocolFound + fileProtocolString.Length)..];
}
return result;
}
}

View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Indexer</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<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\FileExplorer.svg" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\FileExplorer.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FileExplorer.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,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.Win32.UI.Shell.PropertiesSystem;
namespace Microsoft.CmdPal.Ext.Indexer.Native;
internal sealed class NativeHelpers
{
public const uint SEEMASKINVOKEIDLIST = 12;
internal static class PropertyKeys
{
public static readonly PROPERTYKEY PKEYItemNameDisplay = new() { fmtid = new System.Guid("B725F130-47EF-101A-A5F1-02608C9EEBAC"), pid = 10 };
public static readonly PROPERTYKEY PKEYItemUrl = new() { fmtid = new System.Guid("49691C90-7E17-101A-A91C-08002B2ECDA9"), pid = 9 };
public static readonly PROPERTYKEY PKEYKindText = new() { fmtid = new System.Guid("F04BEF95-C585-4197-A2B7-DF46FDC9EE6D"), pid = 100 };
}
internal static class OleDb
{
public static readonly Guid DbGuidDefault = new("C8B521FB-5CF3-11CE-ADE5-00AA0044773D");
}
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
#nullable enable
namespace Microsoft.CmdPal.Ext.Indexer;
/// <summary>
/// This is almost more of just a sample than anything.
/// This is one singular page for switching.
/// </summary>
public sealed partial class DirectoryExplorePage : DynamicListPage
{
private string _path;
private List<ExploreListItem>? _directoryContents;
private List<ExploreListItem>? _filteredContents;
public DirectoryExplorePage(string path)
{
_path = path;
Icon = Icons.FileExplorer;
Name = Resources.Indexer_Command_Browse;
Title = path;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (_directoryContents == null)
{
return;
}
if (string.IsNullOrEmpty(newSearch))
{
if (_filteredContents != null)
{
_filteredContents = null;
RaiseItemsChanged(-1);
}
return;
}
// Need to break this out the manual way so that the compiler can know
// this is an ExploreListItem
var filteredResults = ListHelpers.FilterList(
_directoryContents,
newSearch,
(s, i) => ListHelpers.ScoreListItem(s, i));
if (_filteredContents != null)
{
lock (_filteredContents)
{
ListHelpers.InPlaceUpdateList<ExploreListItem>(_filteredContents, filteredResults);
}
}
else
{
_filteredContents = filteredResults.ToList();
}
RaiseItemsChanged(-1);
}
public override IListItem[] GetItems()
{
if (_filteredContents != null)
{
return _filteredContents.ToArray();
}
if (_directoryContents != null)
{
return _directoryContents.ToArray();
}
IsLoading = true;
if (!Path.Exists(_path))
{
EmptyContent = new CommandItem(title: Resources.Indexer_File_Does_Not_Exist);
return [];
}
var attr = File.GetAttributes(_path);
// detect whether its a directory or file
if ((attr & FileAttributes.Directory) != FileAttributes.Directory)
{
EmptyContent = new CommandItem(title: Resources.Indexer_File_Is_File_Not_Folder);
return [];
}
var contents = Directory.EnumerateFileSystemEntries(_path);
_directoryContents = contents
.Select(s => new IndexerItem() { FullPath = s, FileName = Path.GetFileName(s) })
.Select(i => new ExploreListItem(i))
.ToList();
foreach (var i in _directoryContents)
{
i.PathChangeRequested += HandlePathChangeRequested;
}
_ = Task.Run(() =>
{
foreach (var item in _directoryContents)
{
IconInfo? icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result;
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch
{
}
item.Icon = icon;
}
});
IsLoading = false;
return _directoryContents.ToArray();
}
private void HandlePathChangeRequested(ExploreListItem sender, string path)
{
_directoryContents = null;
_filteredContents = null;
_path = path;
Title = path;
SearchText = string.Empty;
RaiseItemsChanged(-1);
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
#nullable enable
namespace Microsoft.CmdPal.Ext.Indexer;
/// <summary>
/// This is almost more of just a sample than anything.
/// </summary>
internal sealed partial class ExploreListItem : ListItem
{
internal string FilePath { get; private set; }
internal event TypedEventHandler<ExploreListItem, string>? PathChangeRequested;
public ExploreListItem(IndexerItem indexerItem)
: base(new NoOpCommand())
{
FilePath = indexerItem.FullPath;
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
List<CommandContextItem> context = [];
if (indexerItem.IsDirectory())
{
Command = new AnonymousCommand(
() => { PathChangeRequested?.Invoke(this, FilePath); })
{
Result = CommandResult.KeepOpen(),
Name = Resources.Indexer_Command_Browse,
};
context.Add(new CommandContextItem(new DirectoryPage(indexerItem.FullPath)));
}
else
{
Command = new OpenFileCommand(indexerItem);
}
MoreCommands = [
..context,
new CommandContextItem(new OpenWithCommand(indexerItem)),
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
new CommandContextItem(new CopyPathCommand(indexerItem)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
];
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class IndexerPage : DynamicListPage, IDisposable
{
private readonly List<IListItem> _indexerListItems = [];
private SearchQuery _searchQuery = new();
private uint _queryCookie = 10;
public IndexerPage()
{
Id = "com.microsoft.indexer.fileSearch";
Icon = Icons.FileExplorer;
Name = Resources.Indexer_Title;
PlaceholderText = Resources.Indexer_PlaceholderText;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (oldSearch != newSearch)
{
_ = Task.Run(() =>
{
Query(newSearch);
LoadMore();
});
}
}
public override IListItem[] GetItems() => [.. _indexerListItems];
public override void LoadMore()
{
IsLoading = true;
FetchItems(20);
IsLoading = false;
RaiseItemsChanged(_indexerListItems.Count);
}
private void Query(string query)
{
++_queryCookie;
_indexerListItems.Clear();
_searchQuery.SearchResults.Clear();
_searchQuery.CancelOutstandingQueries();
if (query == string.Empty)
{
return;
}
Stopwatch stopwatch = new();
stopwatch.Start();
_searchQuery.Execute(query, _queryCookie);
stopwatch.Stop();
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
}
private void FetchItems(int limit)
{
if (_searchQuery != null)
{
var cookie = _searchQuery.Cookie;
if (cookie == _queryCookie)
{
var index = 0;
SearchResult result;
var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit);
while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit)
{
IconInfo icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get the icon.", ex);
}
_indexerListItems.Add(new IndexerListItem(new IndexerItem
{
FileName = result.ItemDisplayName,
FullPath = result.LaunchUri,
})
{
Icon = icon,
});
}
HasMoreItems = hasMoreItems;
}
}
}
public void Dispose()
{
_searchQuery = null;
GC.SuppressFinalize(this);
}
}