Compare commits

...

21 Commits

Author SHA1 Message Date
Mike Griese
fd9f523d5d Merge branch 'dev/migrie/b/core-run-nits' into dev/migrie/f/run-telem 2025-09-30 13:16:47 -05:00
Mike Griese
de5fee2ca6 Merge remote-tracking branch 'origin/main' into dev/migrie/b/core-run-nits 2025-09-30 13:08:26 -05:00
Mike Griese
8318a40dd4 CmdPal: Bump version to 0.6 (#42097)
This bumps the CmdPal version to 0.6.

It also moves the template project to consume the 0.5 SDK. 

It also removes the WASDK dependency, because we only need the MSIX
tooling.
2025-09-30 12:55:06 -05:00
ruslanlap
f1f00475d1 Add CheatSheets plugin to third-party Run plugins documentation (#41952)
This PR adds the CheatSheets plugin to the third-party plugins
documentation in the General plugins section.

## CheatSheets Plugin
📚 CheatSheets for PowerToys Run - Find cheat sheets and command examples
instantly always at your fingertips with PowerToys Run plugin

![CheatSheets
Demo](https://github.com/ruslanlap/PowerToysRun-CheatSheets/blob/master/assets/demo-cheatsheets.gif)

This plugin enables users to instantly find cheat sheets and command
examples for various tools and programming languages without leaving
PowerToys Run.

### Features
- 🔍 Instant Search - Find commands and cheat sheets with fuzzy matching
- 📚 Multiple Sources - Integrates with tldr, cheat.sh, and offline cheat
sheets
-  Favorites System - Save and quickly access your most-used commands
- 📂 Categories - Browse commands by tool/language (git, docker, python,
etc.)
- 📊 Usage History - Tracks popular commands for quick access
- 💾 Smart Caching - Fast offline access with configurable cache duration
- 🎨 Modern UI - Beautiful WPF interface with theme adaptation
- 🔧 Offline Mode - Works without internet connection using cached data

## Link to plugin
- https://github.com/ruslanlap/PowerToysRun-CheatSheets
2025-09-30 12:53:22 -05:00
Mike Griese
5da2e74622 aot compatible 2025-09-30 12:15:11 -05:00
Jiří Polášek
0d3db48ab1 CmdPal: Properly quote arguments when rebuilding normalized path (#42071)
## Summary of the Pull Request

This PR ensures proper quoting of arguments after normalization. When
joining arguments back into a single string, any argument containing
whitespace or double quotes must be quoted (because parsing unquoted
them). Adjusts unit tests to reflect the correct expected results.

Ref: 42016

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-30 11:03:52 -05:00
Jiří Polášek
c8486087d8 CmdPal: Update visual style of details panel elements (#42102)
## Summary of the Pull Request

This PR updates the details panel formatting:

- Hides the empty text block used as a separator when the key is empty.
- Makes separators more subtle by adjusting the brush.  
- Reverses the typographical hierarchy of detail key/value items, making
the value dominant and the key more subtle to help users focus on the
content.
- Defines new detail text styles derived from the base WinUI
typographical styles.


| Before | After |
|--------|-------|
| <img width="711" height="1795" alt="image"
src="https://github.com/user-attachments/assets/9155ec88-639a-44c1-a70d-edcd4107945e"
/> | <img width="743" height="1667" alt="image"
src="https://github.com/user-attachments/assets/9d1dc432-82da-4183-b347-74a2f3b96c53"
/> |


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #42099 
- [x] Closes: #41664
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-30 09:06:48 -05:00
Mike Griese
d43a23c745 throwing considered harmful 2025-09-30 05:49:39 -05:00
Mike Griese
ffae061135 just move your around 2025-09-30 05:39:29 -05:00
Mike Griese
1e7c60ec23 lets move you 2025-09-29 16:47:35 -05:00
Mike Griese
2a5c61ce1f lots of cleanup 2025-09-29 16:41:44 -05:00
Mike Griese
fdfffdc256 sync this up a little 2025-09-29 16:35:47 -05:00
Mike Griese
f249f99694 wire it all up 2025-09-29 16:14:42 -05:00
Jiří Polášek
05c700a4cd CmdPal: Fix NavView merge (#42096)
## Summary of the Pull Request

Regression: #42044

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-29 23:01:32 +02:00
Mike Griese
28c0d4a420 mock out 2025-09-29 15:36:24 -05:00
Niels Laute
48b70e0861 [CmdPal Settings] Improved NavView behavior (#42044)
## Summary of the Pull Request

The NavView behavior (e.g. when showing the panebutton, collapsing the
menu etc.) was inconsistent with other Settings experiences (like PT
Settings and W11 Settings).

This PR makes use of the TitleBar's PaneToggleButton.

## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-09-29 17:26:24 +02:00
Mike Griese
b0f6e0ae87 didn't need this 2025-09-29 10:15:02 -05:00
Mike Griese
8cb518b649 CmdPal: collection of Run Commands nits
* Path items were being treated inconsistently
* We shouldn't re-enumerate a directory on every keystroke
* A bunch of elements had empty TextToSuggest (which makes it crazier
  that it ever worked right)
2025-09-29 10:12:29 -05:00
Jiří Polášek
08dc3fbcef CmdPal: Fix desynced resmanager files (#42038)
## Summary of the Pull Request

This PR fixes desynced resource manager files introduced by previous
commits.
While Visual Studio would regenerate and correct these files
automatically, applying this fix preemptively reduces unnecessary churn
in unrelated commits and avoids redundant file changes.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-29 10:09:42 -05:00
Jiří Polášek
eeb84cb621 Dependencies: Upgrade WinUIEx to 2.8.0 (#40639)
## Summary of the Pull Request

This change upgrades the WinUIEx NuGet package from version 2.2.0 to
2.8.0.
- Prevents the window itself from taking focus when it should not.
- Removes dead code from Settings.UI (the code triggered error
[WinUIEX1001](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1001.html)
-- Window.Current is always null).
## PR Checklist

- [x] Closes: #40637
- [x] Closes: #7647
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

I've built and run [projects utilizing
WinUIEx](https://github.com/search?q=repo%3Amicrosoft%2FPowerToys+WinUIEx+path%3A*.*proj&type=code):
- Microsoft.CmdPal.UI
- MeasureToolUI
- FileLocksmithUI
- EnvironmentVariables
- AdvancedPaste
- Peek.UI
- RegistryPreview
- PowerToys.Settings
- Hosts
2025-09-29 08:43:57 -05:00
Gordon Lam
5b2388cd58 Add missing sign dll as part of build (#42075)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request updates the `.pipelines/ESRPSigning_core.json` file to
include additional DLLs for code signing. These changes ensure that new
dependencies are properly signed as part of the build and deployment
process.

**Added DLLs for signing:**
* Added `CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll` and its
`WinUI3Apps` version to the list of files to be signed.
* Added `Markdig.dll` and its `WinUI3Apps` version to the list of files
to be signed.
* Added `RomanNumerals.dll` and its `WinUI3Apps` version to the list of
files to be signed.
2025-09-29 18:17:44 +08:00
41 changed files with 674 additions and 722 deletions

View File

@@ -210,6 +210,7 @@ capturevideosample
cmdow
Controlz
cortana
devhints
dlnilsson
fancymouse
firefox
@@ -229,6 +230,7 @@ regedit
roslyn
Skia
Spotify
tldr
Vanara
wangyi
WEX

View File

@@ -327,6 +327,12 @@
"WinUI3Apps\\ReverseMarkdown.dll",
"WinUI3Apps\\SharpCompress.dll",
"WinUI3Apps\\ZstdSharp.dll",
"CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
"WinUI3Apps\\CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
"Markdig.dll",
"WinUI3Apps\\Markdig.dll",
"RomanNumerals.dll",
"WinUI3Apps\\RomanNumerals.dll",
"TestableIO.System.IO.Abstractions.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.dll",
"TestableIO.System.IO.Abstractions.Wrappers.dll",

View File

@@ -108,7 +108,7 @@
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.2.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />

View File

@@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
## Extending software plugins

View File

@@ -2,7 +2,10 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CmdPalVersion Condition="'$(CmdPalVersion)'=='' and '$(XES_APPXMANIFESTVERSION)'!=''">$(XES_APPXMANIFESTVERSION)</CmdPalVersion>
<!-- MIKE: The file you're looking for is src/modules/cmdpal/custom.props -->
<CmdPalVersion Condition="'$(CmdPalVersion)'==''">0.0.1.0</CmdPalVersion>
<DevEnvironment>Local</DevEnvironment>
<!-- Forcing for every DLL on by default -->

View File

@@ -2,8 +2,6 @@
// The 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.Core.Common.Services;
public interface IRunHistoryService
@@ -25,3 +23,12 @@ public interface IRunHistoryService
/// <param name="item">The run history item to add.</param>
void AddRunHistoryItem(string item);
}
public interface ITelemetryService
{
void LogRunQuery(string query, int resultCount, ulong durationMs);
void LogRunCommand(string command, bool asAdmin, bool success);
void LogOpenUri(string uri, bool isWeb, bool success);
}

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -3,12 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<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.3.183" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />

View File

@@ -40,10 +40,13 @@
<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" />
<PackageReference Include="Shmuelie.WinRTServer" />
<!-- Needed to enable building an MSIX package -->
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.MSIX">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<!--
@@ -84,6 +87,9 @@
<!-- In Release builds, trimming is enabled by default.
feel free to disable this if needed -->
<PublishTrimmed>true</PublishTrimmed>
<!-- In release, also ignore the aforementioned ILLink warning -->
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -160,7 +160,7 @@ public partial class App : Application
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
services.AddSingleton(new TelemetryForwarder());
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
// ViewModels
services.AddSingleton<ShellViewModel>();

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
// Just put all the run events in one file for simplicity.
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable SA1649 // File name should match first type name
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunQuery : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Query { get; set; }
public int ResultCount { get; set; }
public ulong DurationMs { get; set; }
public CmdPalRunQuery(string query, int resultCount, ulong durationMs)
{
EventName = "CmdPal_RunQuery";
Query = query;
ResultCount = resultCount;
DurationMs = durationMs;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunCommand : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Command { get; set; }
public bool AsAdmin { get; set; }
public bool Success { get; set; }
public CmdPalRunCommand(string command, bool asAdmin, bool success)
{
EventName = "CmdPal_RunCommand";
Command = command;
AsAdmin = asAdmin;
Success = success;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalOpenUri : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Uri { get; set; }
public bool IsWeb { get; set; }
public bool Success { get; set; }
public CmdPalOpenUri(string uri, bool isWeb, bool success)
{
EventName = "CmdPal_OpenUri";
Uri = uri;
IsWeb = isWeb;
Success = success;
}
}
#pragma warning restore SA1649 // File name should match first type name
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -2,9 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.UI.Helpers;
internal static class BindTransformers
{
public static bool Negate(bool value) => !value;
public static Visibility EmptyToCollapsed(string? input)
=> string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
@@ -19,6 +20,7 @@ namespace Microsoft.CmdPal.UI;
/// or something similar, but this works for now.
/// </summary>
internal sealed class TelemetryForwarder :
ITelemetryService,
IRecipient<BeginInvokeMessage>,
IRecipient<CmdPalInvokeResultMessage>
{
@@ -37,4 +39,19 @@ internal sealed class TelemetryForwarder :
{
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
}
public void LogRunQuery(string query, int resultCount, ulong durationMs)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs));
}
public void LogRunCommand(string command, bool asAdmin, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunCommand(command, asAdmin, success));
}
public void LogOpenUri(string uri, bool isWeb, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalOpenUri(uri, isWeb, success));
}
}

View File

@@ -41,6 +41,31 @@
FalseValue="Visible"
TrueValue="Collapsed" />
<Style
x:Key="DetailKeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="SeparatorKeyTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<Style
x:Key="DetailValueTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag
HorizontalAlignment="Left"
@@ -68,7 +93,7 @@
Margin="0,3,8,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock Text="{x:Bind Name}" />
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
</StackPanel>
</Button>
</StackPanel>
@@ -76,20 +101,13 @@
<DataTemplate x:Key="DetailsLinkTemplate" x:DataType="coreViewModels:DetailsLinkViewModel">
<StackPanel Orientation="Vertical">
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource DetailValueTextBlockStyle}"
Text="{x:Bind Text, Mode=OneWay}"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
FontSize="12"
NavigateUri="{x:Bind Link, Mode=OneWay}"
Visibility="{x:Bind IsLink, Mode=OneWay}">
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />
@@ -98,10 +116,7 @@
</DataTemplate>
<DataTemplate x:Key="DetailsCommandsTemplate" x:DataType="coreViewModels:DetailsCommandsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource CommandTemplate}"
ItemsSource="{x:Bind Commands, Mode=OneWay}"
@@ -111,24 +126,20 @@
<DataTemplate x:Key="DetailsSeparatorTemplate" x:DataType="coreViewModels:DetailsSeparatorViewModel">
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
<Border
Margin="8,0,0,0"
BorderBrush="{ThemeResource TextFillColorSecondaryBrush}"
Margin="0,0,0,0"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,2">
<TextBlock
Margin="-8,0,0,8"
FontWeight="SemiBold"
IsTextSelectionEnabled="True"
Margin="0,0,0,0"
Style="{StaticResource SeparatorKeyTextBlockStyle}"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" />
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags, Mode=OneWay}"

View File

@@ -24,10 +24,11 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="TitleBar">
<TitleBar x:Name="AppTitleBar" PaneToggleRequested="AppTitleBar_PaneToggleRequested">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
x:Name="WorkAroundIcon"
Height="16"
Margin="16,0,0,0"
Source="ms-appx:///Assets/icon.svg" />
@@ -36,18 +37,19 @@
<NavigationView
x:Name="NavView"
Grid.Row="1"
CompactModeThresholdWidth="1007"
DisplayModeChanged="NavView_DisplayModeChanged"
ExpandedModeThresholdWidth="1007"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
ItemInvoked="NavView_ItemInvoked"
Loaded="NavView_Loaded"
OpenPaneLength="200">
Loaded="NavView_Loaded">
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
<Thickness x:Key="NavigationViewHeaderMargin">15,0,0,0</Thickness>
</NavigationView.Resources>
<NavigationView.MenuItems>
<NavigationViewItem
x:Uid="Settings_GeneralPage_NavigationViewItem_General"
@@ -58,34 +60,32 @@
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
Grid.Row="0"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
<Grid Padding="16,0">
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
</Grid>
<Frame x:Name="NavFrame" Grid.Row="1" />
</Grid>
</NavigationView>

View File

@@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar;
namespace Microsoft.CmdPal.UI.Settings;
@@ -34,7 +35,7 @@ public sealed partial class SettingsWindow : WindowEx,
var title = RS_.GetString("SettingsWindowTitle");
this.AppWindow.Title = title;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.TitleBar.Title = title;
this.AppTitleBar.Title = title;
PositionCentered();
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
@@ -142,11 +143,13 @@ public sealed partial class SettingsWindow : WindowEx,
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
NavView.IsPaneToggleButtonVisible = false;
AppTitleBar.IsPaneToggleButtonVisible = true;
WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment
}
else
{
NavView.IsPaneToggleButtonVisible = true;
AppTitleBar.IsPaneToggleButtonVisible = false;
WorkAroundIcon.Margin = new Thickness(16, 0, 0, 0); // Required for workaround, see XAML comment
}
}
@@ -155,6 +158,11 @@ public sealed partial class SettingsWindow : WindowEx,
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
}
private void AppTitleBar_PaneToggleRequested(TitleBar sender, object args)
{
NavView.IsPaneOpen = !NavView.IsPaneOpen;
}
}
public readonly struct Crumb

View File

@@ -23,6 +23,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[DataRow("ping bing.com", "c:\\Windows\\system32\\ping.exe", "bing.com")]
[DataRow("curl bing.com", "c:\\Windows\\system32\\curl.exe", "bing.com")]
[DataRow("ipconfig /all", "c:\\Windows\\system32\\ipconfig.exe", "/all")]
[DataRow("ipconfig a b \"c d\"", "c:\\Windows\\system32\\ipconfig.exe", "a b \"c d\"")]
public void NormalizeCommandLineSimple(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);
@@ -46,7 +47,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[TestMethod]
[DataRow("cmd --run --test", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd --run --test ", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "--run --test --pass")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "\"--run --test\" --pass")]
public void NormalizeArgsWithSpaces(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);

View File

@@ -2,7 +2,6 @@
// The 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;
@@ -83,7 +82,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistory = CreateMockHistoryService();
var pages = new ShellListPage(settings, mockHistory.Object);
var pages = new ShellListPage(settings, mockHistory.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -115,7 +114,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -141,7 +140,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{

View File

@@ -16,7 +16,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Assert
Assert.IsNotNull(provider.DisplayName);
@@ -28,7 +28,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Assert
Assert.IsNotNull(provider.Icon);
@@ -39,7 +39,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Act
var commands = provider.TopLevelCommands();

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>5</VersionMinor>
<VersionMinor>6</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -1,228 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Commands;
internal sealed partial class ExecuteItem : InvokableCommand
{
private readonly ISettingsInterface _settings;
private readonly RunAsType _runas;
public string Cmd { get; internal set; } = string.Empty;
private static readonly char[] Separator = [' '];
public ExecuteItem(string cmd, ISettingsInterface settings, RunAsType type = RunAsType.None)
{
if (type == RunAsType.Administrator)
{
Name = Properties.Resources.cmd_run_as_administrator;
Icon = Icons.AdminIcon;
}
else if (type == RunAsType.OtherUser)
{
Name = Properties.Resources.cmd_run_as_user;
Icon = Icons.UserIcon;
}
else
{
Name = Properties.Resources.generic_run_command;
Icon = Icons.RunV2Icon;
}
Cmd = cmd;
_settings = settings;
_runas = type;
}
private void Execute(Func<ProcessStartInfo, Process?> startProcess, ProcessStartInfo info)
{
if (startProcess is null)
{
return;
}
try
{
startProcess(info);
}
catch (FileNotFoundException e)
{
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
var message = $"{Properties.Resources.cmd_command_not_found}: {e.Message}";
// GH TODO #138 -- show this message once that's wired up
// _context.API.ShowMsg(name, message);
}
catch (Win32Exception e)
{
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
var message = $"{Properties.Resources.cmd_command_failed}: {e.Message}";
ExtensionHost.LogMessage(new LogMessage() { Message = name + message });
// GH TODO #138 -- show this message once that's wired up
// _context.API.ShowMsg(name, message);
}
}
public static ProcessStartInfo SetProcessStartInfo(string fileName, string workingDirectory = "", string arguments = "", string verb = "")
{
var info = new ProcessStartInfo
{
FileName = fileName,
WorkingDirectory = workingDirectory,
Arguments = arguments,
Verb = verb,
};
return info;
}
private ProcessStartInfo PrepareProcessStartInfo(string command, RunAsType runAs = RunAsType.None)
{
command = Environment.ExpandEnvironmentVariables(command);
var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
// Set runAsArg
var runAsVerbArg = string.Empty;
if (runAs == RunAsType.OtherUser)
{
runAsVerbArg = "runAsUser";
}
else if (runAs == RunAsType.Administrator || _settings.RunAsAdministrator)
{
runAsVerbArg = "runAs";
}
if (Enum.TryParse<ExecutionShell>(_settings.ShellCommandExecution, out var executionShell))
{
ProcessStartInfo info;
if (executionShell == ExecutionShell.Cmd)
{
var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause";
info = SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.Powershell)
{
var arguments = _settings.LeaveShellOpen
? $"-NoExit \"{command}\""
: $"\"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
info = SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.PowerShellSeven)
{
var arguments = _settings.LeaveShellOpen
? $"-NoExit -C \"{command}\""
: $"-C \"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
info = SetProcessStartInfo("pwsh.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalCmd)
{
var arguments = _settings.LeaveShellOpen ? $"cmd.exe /k \"{command}\"" : $"cmd.exe /c \"{command}\" & pause";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalPowerShell)
{
var arguments = _settings.LeaveShellOpen ? $"powershell -NoExit -C \"{command}\"" : $"powershell -C \"{command}\"";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalPowerShellSeven)
{
var arguments = _settings.LeaveShellOpen ? $"pwsh.exe -NoExit -C \"{command}\"" : $"pwsh.exe -C \"{command}\"";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.RunCommand)
{
// Open explorer if the path is a file or directory
if (Directory.Exists(command) || File.Exists(command))
{
info = SetProcessStartInfo("explorer.exe", arguments: command, verb: runAsVerbArg);
}
else
{
var parts = command.Split(Separator, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ShellListPageHelpers.FileExistInPath(filename))
{
var arguments = parts[1];
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{filename} {arguments}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(filename, workingDirectory, arguments, runAsVerbArg);
}
}
else
{
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(command, verb: runAsVerbArg);
}
}
}
else
{
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(command, verb: runAsVerbArg);
}
}
}
}
else
{
throw new NotImplementedException();
}
info.UseShellExecute = true;
_settings.AddCmdHistory(command);
return info;
}
else
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Error extracting setting" });
throw new NotImplementedException();
}
}
public override CommandResult Invoke()
{
try
{
Execute(Process.Start, PrepareProcessStartInfo(Cmd, _runas));
}
catch
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Error starting the process " });
}
return CommandResult.Dismiss();
}
}

View File

@@ -2,9 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
@@ -13,18 +11,33 @@ internal sealed partial class OpenUrlWithHistoryCommand : OpenUrlCommand
{
private readonly Action<string>? _addToHistory;
private readonly string _url;
private readonly ITelemetryService? _telemetryService;
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null)
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null, ITelemetryService? telemetryService = null)
: base(url)
{
_addToHistory = addToHistory;
_url = url;
_telemetryService = telemetryService;
}
public override CommandResult Invoke()
{
_addToHistory?.Invoke(_url);
var result = base.Invoke();
return result;
var success = ShellHelpers.OpenInShell(_url);
var isWebUrl = false;
if (Uri.TryCreate(_url, UriKind.Absolute, out var uri))
{
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
isWebUrl = true;
}
}
_telemetryService?.LogOpenUri(_url, isWebUrl, success);
return CommandResult.Dismiss();
}
}

View File

@@ -2,14 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
@@ -19,18 +14,20 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
private static readonly char[] _systemDirectoryRoots = ['\\', '/'];
private readonly Action<string>? _addToHistory;
private readonly ITelemetryService _telemetryService;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentUpdateTask;
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory)
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory, ITelemetryService telemetryService)
: base(
new NoOpCommand() { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title)
ResourceLoaderInstance.GetString("shell_command_display_title"))
{
Title = string.Empty;
Subtitle = Properties.Resources.generic_run_command;
Subtitle = ResourceLoaderInstance.GetString("generic_run_command");
Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon.
_addToHistory = addToHistory;
_telemetryService = telemetryService;
}
public override void UpdateQuery(string query)
@@ -147,7 +144,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory);
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory, telemetryService: _telemetryService);
Title = exeItem.Title;
Subtitle = exeItem.Subtitle;
Icon = exeItem.Icon;
@@ -156,7 +153,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, _addToHistory);
var pathItem = new PathListItem(exe, query, _addToHistory, _telemetryService);
Command = pathItem.Command;
MoreCommands = pathItem.MoreCommands;
Title = pathItem.Title;
@@ -165,7 +162,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory) { Result = CommandResult.Dismiss() };
Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory, _telemetryService) { Result = CommandResult.Dismiss() };
Title = searchText;
}
else

View File

@@ -2,10 +2,10 @@
// The 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.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.FileSystem;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -19,38 +19,11 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public static class CommandLineNormalizer
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const int MAX_PATH = 260;
private const uint INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF;
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x10;
private const int MAX_PATH = 260;
#pragma warning restore SA1310 // Field names should not contain underscore
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint ExpandEnvironmentStringsW(
[MarshalAs(UnmanagedType.LPWStr)] string lpSrc,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpDst,
uint nSize);
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr CommandLineToArgvW(
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
out int pNumArgs);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint SearchPathW(
[MarshalAs(UnmanagedType.LPWStr)] string? lpPath,
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
[MarshalAs(UnmanagedType.LPWStr)] string? lpExtension,
uint nBufferLength,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpBuffer,
out IntPtr lpFilePart);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetFileAttributesW(
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr hMem);
/// <summary>
/// Normalizes a command line string by expanding environment variables, resolving executable paths,
/// and standardizing the format for comparison purposes.
@@ -129,9 +102,9 @@ public static class CommandLineNormalizer
private static string ExpandEnvironmentVariables(string input)
{
const int initialBufferSize = 1024;
var buffer = new StringBuilder(initialBufferSize);
var buffer = new char[initialBufferSize];
var result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
var result = PInvoke.ExpandEnvironmentStrings(input, buffer);
if (result == 0)
{
@@ -139,11 +112,11 @@ public static class CommandLineNormalizer
return input;
}
if (result > buffer.Capacity)
if (result > buffer.Length)
{
// Buffer was too small, resize and try again
buffer.Capacity = (int)result;
result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
buffer = new char[result];
result = PInvoke.ExpandEnvironmentStrings(input, buffer);
if (result == 0)
{
@@ -151,7 +124,7 @@ public static class CommandLineNormalizer
}
}
return buffer.ToString();
return new string(buffer, 0, (int)result - 1); // -1 to exclude null terminator
}
/// <summary>
@@ -159,28 +132,30 @@ public static class CommandLineNormalizer
/// </summary>
private static string[] ParseCommandLineToArguments(string commandLine)
{
var argv = CommandLineToArgvW(commandLine, out var argc);
if (argv == IntPtr.Zero || argc == 0)
unsafe
{
return Array.Empty<string>();
}
var argv = PInvoke.CommandLineToArgv(commandLine, out var argc);
try
{
var args = new string[argc];
for (var i = 0; i < argc; i++)
if (argv == null || argc == 0)
{
var argPtr = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
args[i] = Marshal.PtrToStringUni(argPtr) ?? string.Empty;
return Array.Empty<string>();
}
return args;
}
finally
{
LocalFree(argv);
try
{
var args = new string[argc];
for (var i = 0; i < argc; i++)
{
args[i] = new string(argv[i]);
}
return args;
}
finally
{
PInvoke.LocalFree(new HLOCAL(argv));
}
}
}
@@ -227,39 +202,46 @@ public static class CommandLineNormalizer
/// </summary>
private static string TryResolveExecutable(string executableName)
{
var buffer = new StringBuilder(MAX_PATH);
var buffer = new char[MAX_PATH];
var result = SearchPathW(
null, // Use default search path
executableName,
".exe", // Default extension
(uint)buffer.Capacity,
buffer,
out var _); // We don't need the file part
if (result == 0)
unsafe
{
return string.Empty;
}
var outParam = default(PWSTR); // ultimately discarded
if (result > buffer.Capacity)
{
// Buffer was too small, resize and try again
buffer.Capacity = (int)result;
result = SearchPathW(null, executableName, ".exe", (uint)buffer.Capacity, buffer, out var _);
var result = PInvoke.SearchPath(
null, // Use default search path
executableName,
".exe", // Default extension
buffer,
&outParam); // We don't need the file part
if (result == 0)
{
return string.Empty;
}
if (result > buffer.Length)
{
// Buffer was too small, resize and try again
buffer = new char[result];
result = PInvoke.SearchPath(null, executableName, ".exe", buffer, &outParam);
if (result == 0)
{
return string.Empty;
}
}
var resolvedPath = new string(buffer, 0, (int)result);
// Verify the resolved path exists and is not a directory
var attributes = PInvoke.GetFileAttributes(resolvedPath);
return attributes == INVALID_FILE_ATTRIBUTES ||
(attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0 ?
string.Empty :
resolvedPath;
}
var resolvedPath = buffer.ToString();
// Verify the resolved path exists and is not a directory
var attributes = GetFileAttributesW(resolvedPath);
return attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? string.Empty : resolvedPath;
}
/// <summary>

View File

@@ -2,13 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Shell.Commands;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,37 +10,6 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public class ShellListPageHelpers
{
private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times);
private readonly ISettingsInterface _settings;
public ShellListPageHelpers(ISettingsInterface settings)
{
_settings = settings;
}
private ListItem GetCurrentCmd(string cmd)
{
var result = new ListItem(new ExecuteItem(cmd, _settings))
{
Title = cmd,
Subtitle = Properties.Resources.cmd_plugin_name + ": " + Properties.Resources.cmd_execute_through_shell,
Icon = new IconInfo(string.Empty),
};
return result;
}
public List<CommandContextItem> LoadContextMenus(ListItem listItem)
{
var resultList = new List<CommandContextItem>
{
new(new ExecuteItem(listItem.Title, _settings, RunAsType.Administrator)),
new(new ExecuteItem(listItem.Title, _settings, RunAsType.OtherUser )),
};
return resultList;
}
internal static bool FileExistInPath(string filename)
{
return FileExistInPath(filename, out var _);
@@ -58,7 +21,7 @@ public class ShellListPageHelpers
return ShellHelpers.FileExistInPath(filename, out fullPath, token ?? CancellationToken.None);
}
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory)
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory, ITelemetryService? telemetryService)
{
var li = new ListItem();
@@ -100,7 +63,7 @@ public class ShellListPageHelpers
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory);
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory, telemetryService);
li.Command = exeItem.Command;
li.Title = exeItem.Title;
li.Subtitle = exeItem.Subtitle;
@@ -109,7 +72,7 @@ public class ShellListPageHelpers
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, addToHistory);
var pathItem = new PathListItem(exe, query, addToHistory, telemetryService);
li.Command = pathItem.Command;
li.Title = pathItem.Title;
li.Subtitle = pathItem.Subtitle;
@@ -118,7 +81,7 @@ public class ShellListPageHelpers
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
li.Command = new OpenUrlWithHistoryCommand(searchText) { Result = CommandResult.Dismiss() };
li.Command = new OpenUrlWithHistoryCommand(searchText, addToHistory, telemetryService) { Result = CommandResult.Dismiss() };
li.Title = searchText;
}
else
@@ -157,7 +120,98 @@ public class ShellListPageHelpers
executable = segments[0];
if (segments.Length > 1)
{
arguments = string.Join(' ', segments[1..]);
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
}
}
private static class ArgumentBuilder
{
internal static string BuildArguments(string[] arguments)
{
if (arguments.Length <= 0)
{
return string.Empty;
}
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(' ');
}
if (argument.Length == 0 || ShouldBeQuoted(argument))
{
stringBuilder.Append('\"');
var index = 0;
while (index < argument.Length)
{
var c = argument[index++];
if (c == '\\')
{
var numBackSlash = 1;
while (index < argument.Length && argument[index] == '\\')
{
index++;
numBackSlash++;
}
if (index == argument.Length)
{
stringBuilder.Append('\\', numBackSlash * 2);
}
else if (argument[index] == '\"')
{
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
stringBuilder.Append('\"');
index++;
}
else
{
stringBuilder.Append('\\', numBackSlash);
}
continue;
}
if (c == '\"')
{
stringBuilder.Append('\\');
stringBuilder.Append('\"');
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append('\"');
}
else
{
stringBuilder.Append(argument);
}
}
private static bool ShouldBeQuoted(string s)
{
foreach (var c in s)
{
if (char.IsWhiteSpace(c) || c == '\"')
{
return true;
}
}
return false;
}
}
}

View File

@@ -1,11 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.CmdPal.Ext.Shell</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -16,7 +12,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"comInterop": {
"preserveSigMethods": [ "*" ]
}
}

View File

@@ -0,0 +1,22 @@
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
SFBS_FLAGS
MAX_PATH
GetDpiForWindow
GetWindowRect
GetMonitorInfo
SetWindowPos
MonitorFromWindow
SHOW_WINDOW_CMD
ShellExecuteEx
SEE_MASK_INVOKEIDLIST
ExpandEnvironmentStringsW
CommandLineToArgvW
SearchPathW
GetFileAttributesW
LocalFree
FILE_FLAGS_AND_ATTRIBUTES

View File

@@ -2,9 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CmdPal.Core.Common.Commands;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
@@ -13,13 +12,18 @@ namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class PathListItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly bool _isDirectory;
private readonly Lazy<bool> fetchedIcon;
private readonly bool isDirectory;
private readonly string path;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public override IIconInfo? Icon { get => fetchedIcon.Value ? _icon : _icon; set => base.Icon = value; }
public PathListItem(string path, string originalDir, Action<string>? addToHistory)
: base(new OpenUrlWithHistoryCommand(path, addToHistory))
private IIconInfo? _icon;
internal bool IsDirectory => isDirectory;
public PathListItem(string path, string originalDir, Action<string>? addToHistory, ITelemetryService? telemetryService = null)
: base(new OpenUrlWithHistoryCommand(path, addToHistory, telemetryService))
{
var fileName = Path.GetFileName(path);
if (string.IsNullOrEmpty(fileName))
@@ -27,8 +31,8 @@ internal sealed partial class PathListItem : ListItem
fileName = Path.GetFileName(Path.GetDirectoryName(path)) ?? string.Empty;
}
_isDirectory = Directory.Exists(path);
if (_isDirectory)
isDirectory = Directory.Exists(path);
if (isDirectory)
{
if (!path.EndsWith('\\'))
{
@@ -41,6 +45,8 @@ internal sealed partial class PathListItem : ListItem
}
}
this.path = path;
Title = fileName; // Just the name of the file is the Title
Subtitle = path; // What the user typed is the subtitle
@@ -58,23 +64,35 @@ internal sealed partial class PathListItem : ListItem
// wrap it in quotes
suggestion = string.Concat("\"", suggestion, "\"");
}
else
{
suggestion = path;
}
TextToSuggest = suggestion;
MoreCommands = [
new CommandContextItem(new OpenWithCommand(path)),
new CommandContextItem(new ShowFileInFolderCommand(path)) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E) },
new CommandContextItem(new CopyPathCommand(path) { Name = Properties.Resources.copy_path_command_name }) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C) },
new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C) },
new CommandContextItem(new OpenInConsoleCommand(path)) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R) },
new CommandContextItem(new OpenPropertiesCommand(path)),
];
_icon = new Lazy<IconInfo>(() =>
fetchedIcon = new Lazy<bool>(() =>
{
var iconStream = ThumbnailHelper.GetThumbnail(path).Result;
var icon = iconStream is not null ? IconInfo.FromStream(iconStream) :
_isDirectory ? Icons.FolderIcon : Icons.RunV2Icon;
return icon;
_ = Task.Run(FetchIconAsync);
return true;
});
}
private async Task FetchIconAsync()
{
var iconStream = await ThumbnailHelper.GetThumbnail(path);
var icon = iconStream != null ?
IconInfo.FromStream(iconStream) :
isDirectory ? Icons.FolderIcon : Icons.RunV2Icon;
_icon = icon;
OnPropertyChanged(nameof(Icon));
}
}

View File

@@ -2,8 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
@@ -15,6 +15,7 @@ internal sealed partial class RunExeItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly Action<string>? _addToHistory;
private readonly ITelemetryService? _telemetryService;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
@@ -26,13 +27,18 @@ internal sealed partial class RunExeItem : ListItem
private string FullString => string.IsNullOrEmpty(_args) ? Exe : $"{Exe} {_args}";
public RunExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
public RunExeItem(
string exe,
string args,
string fullExePath,
Action<string>? addToHistory,
ITelemetryService? telemetryService = null)
{
FullExePath = fullExePath;
Exe = exe;
var command = new AnonymousCommand(Run)
{
Name = Properties.Resources.generic_run_command,
Name = ResourceLoaderInstance.GetString("generic_run_command"),
Result = CommandResult.Dismiss(),
};
Command = command;
@@ -46,6 +52,7 @@ internal sealed partial class RunExeItem : ListItem
});
_addToHistory = addToHistory;
_telemetryService = telemetryService;
UpdateArgs(args);
@@ -53,13 +60,13 @@ internal sealed partial class RunExeItem : ListItem
new CommandContextItem(
new AnonymousCommand(RunAsAdmin)
{
Name = Properties.Resources.cmd_run_as_administrator,
Name = ResourceLoaderInstance.GetString("cmd_run_as_administrator"),
Icon = Icons.AdminIcon,
}) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter) },
new CommandContextItem(
new AnonymousCommand(RunAsOther)
{
Name = Properties.Resources.cmd_run_as_user,
Name = ResourceLoaderInstance.GetString("cmd_run_as_user"),
Icon = Icons.UserIcon,
}) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U) },
];
@@ -97,20 +104,26 @@ internal sealed partial class RunExeItem : ListItem
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args);
var success = ShellHelpers.OpenInShell(FullExePath, _args);
_telemetryService?.LogRunCommand(FullString, false, success);
}
public void RunAsAdmin()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
var success = ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
_telemetryService?.LogRunCommand(FullString, true, success);
}
public void RunAsOther()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
var success = ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
_telemetryService?.LogRunCommand(FullString, false, success);
}
}

View File

@@ -2,15 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,13 +11,13 @@ namespace Microsoft.CmdPal.Ext.Shell.Pages;
internal sealed partial class ShellListPage : DynamicListPage, IDisposable
{
private readonly ShellListPageHelpers _helper;
private readonly List<ListItem> _topLevelItems = [];
private readonly Dictionary<string, ListItem> _historyItems = [];
private readonly List<ListItem> _currentHistoryItems = [];
private readonly IRunHistoryService _historyService;
private readonly ITelemetryService? _telemetryService;
private readonly Dictionary<string, ListItem> _currentPathItems = new();
private ListItem? _exeItem;
private List<ListItem> _pathItems = [];
@@ -35,27 +28,26 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private bool _loadedInitialHistory;
public ShellListPage(ISettingsInterface settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
private string _currentSubdir = string.Empty;
public ShellListPage(
ISettingsInterface settingsManager,
IRunHistoryService runHistoryService,
ITelemetryService? telemetryService)
{
Icon = Icons.RunV2Icon;
Id = "com.microsoft.cmdpal.shell";
Name = Resources.cmd_plugin_name;
PlaceholderText = Resources.list_placeholder_text;
_helper = new(settingsManager);
Name = ResourceLoaderInstance.GetString("cmd_plugin_name");
PlaceholderText = ResourceLoaderInstance.GetString("list_placeholder_text");
_historyService = runHistoryService;
_telemetryService = telemetryService;
EmptyContent = new CommandItem()
{
Title = Resources.cmd_plugin_name,
Title = ResourceLoaderInstance.GetString("cmd_plugin_name"),
Icon = Icons.RunV2Icon,
Subtitle = Resources.list_placeholder_text,
Subtitle = ResourceLoaderInstance.GetString("list_placeholder_text"),
};
if (addBuiltins)
{
// here, we _could_ add built-in providers if we wanted. links to apps, calc, etc.
// That would be a truly run-first experience
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
@@ -123,8 +115,13 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private async Task BuildListItemsForSearchAsync(string newSearch, CancellationToken cancellationToken)
{
var timer = System.Diagnostics.Stopwatch.StartNew();
// Check for cancellation at the start
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// If the search text is the start of a path to a file (it might be a
// UNC path), then we want to list all the files that start with that text:
@@ -136,7 +133,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
var expanded = Environment.ExpandEnvironmentVariables(searchText);
// Check for cancellation after environment expansion
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// TODO we can be smarter about only re-reading the filesystem if the
// new search is just the oldSearch+some chars
@@ -206,7 +206,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
couldResolvePath = false;
}
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
_pathItems.Clear();
@@ -221,7 +224,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
// Check for cancellation before creating exe items
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
if (couldResolvePath && exeExists)
{
@@ -278,17 +284,31 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
_currentHistoryItems.AddRange(filteredHistory);
// Final cancellation check
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
timer.Stop();
_telemetryService?.LogRunQuery(newSearch, GetItems().Length, (ulong)timer.ElapsedMilliseconds);
}
private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null)
private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null, ITelemetryService? telemetryService = null)
{
var pathItem = new PathListItem(path, originalPath, addToHistory);
var pathItem = new PathListItem(path, originalPath, addToHistory, telemetryService);
if (pathItem.IsDirectory)
{
return pathItem;
}
// Is this path an executable? If so, then make a RunExeItem
if (IsExecutable(path))
{
var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory);
var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory, telemetryService)
{
TextToSuggest = path,
};
exeItem.MoreCommands = [
.. exeItem.MoreCommands,
@@ -306,24 +326,22 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
LoadInitialHistory();
}
var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText);
List<ListItem> uriItems = _uriItem is not null ? [_uriItem] : [];
List<ListItem> exeItems = _exeItem is not null ? [_exeItem] : [];
return
exeItems
.Concat(filteredTopLevel)
.Concat(_currentHistoryItems)
.Concat(_pathItems)
.Concat(uriItems)
.ToArray();
}
internal static ListItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
internal static ListItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory, ITelemetryService? telemetryService)
{
// PathToListItem will return a RunExeItem if it can find a executable.
// It will ALSO add the file search commands to the RunExeItem.
return PathToListItem(fullExePath, exe, args, addToHistory);
return PathToListItem(fullExePath, exe, args, addToHistory, telemetryService);
}
private void CreateAndAddExeItems(string exe, string args, string fullExePath)
@@ -335,7 +353,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
else
{
_exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory);
_exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory, _telemetryService);
}
}
@@ -389,7 +407,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
// Check for cancellation before directory operations
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
var dirExists = Directory.Exists(directoryPath);
@@ -408,30 +429,71 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
if (dirExists)
{
// Check for cancellation before file system enumeration
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
if (directoryPath == _currentSubdir)
{
// Filter the items we already had
var fuzzyString = searchPattern.TrimEnd('*');
var newMatchedPathItems = new List<ListItem>();
foreach (var kv in _currentPathItems)
{
var score = string.IsNullOrEmpty(fuzzyString) ?
1 :
FuzzyStringMatcher.ScoreFuzzy(fuzzyString, kv.Key);
if (score > 0)
{
newMatchedPathItems.Add(kv.Value);
}
}
ListHelpers.InPlaceUpdateList(_pathItems, newMatchedPathItems);
return;
}
// Get all the files in the directory that start with the search text
// Run this on a background thread to avoid blocking
var files = await Task.Run(() => Directory.GetFileSystemEntries(directoryPath, searchPattern), cancellationToken);
// Check for cancellation after file enumeration
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
var searchPathTrailer = trimmed.Remove(0, Math.Min(directoryPath.Length, trimmed.Length));
var originalBeginning = originalPath.Remove(originalPath.Length - searchPathTrailer.Length);
var originalBeginning = originalPath.EndsWith(searchPathTrailer, StringComparison.CurrentCultureIgnoreCase) ?
originalPath.Remove(originalPath.Length - searchPathTrailer.Length) :
originalPath;
if (isDriveRoot)
{
originalBeginning = string.Concat(originalBeginning, '\\');
}
// Create a list of commands for each file
var commands = files.Select(f => PathToListItem(f, originalBeginning)).ToList();
var newPathItems = files
.Select(f => PathToListItem(f, originalBeginning))
.ToDictionary(item => item.Title, item => item);
// Final cancellation check before updating results
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// Add the commands to the list
_pathItems = commands;
_pathItems = newPathItems.Values.ToList();
_currentSubdir = directoryPath;
_currentPathItems.Clear();
foreach ((var k, IListItem v) in newPathItems)
{
_currentPathItems[k] = (ListItem)v;
}
}
else
{
@@ -458,7 +520,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
{
var hist = _historyService.GetRunHistory();
var histItems = hist
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory)))
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory, _telemetryService)))
.Where(tuple => tuple.Item2 is not null)
.Select(tuple => (tuple.h, tuple.Item2!))
.ToList();

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Shell;
internal static class ResourceLoaderInstance
{
public static string GetString(string resourceKey)
{
return Properties.Resources.ResourceManager.GetString(resourceKey, Properties.Resources.Culture) ?? throw new InvalidOperationException($"Resource key '{resourceKey}' not found.");
}
}

View File

@@ -19,19 +19,21 @@ public partial class ShellCommandsProvider : CommandProvider
private readonly ShellListPage _shellListPage;
private readonly FallbackCommandItem _fallbackItem;
private readonly IRunHistoryService _historyService;
private readonly ITelemetryService _telemetryService;
public ShellCommandsProvider(IRunHistoryService runHistoryService)
public ShellCommandsProvider(IRunHistoryService runHistoryService, ITelemetryService telemetryService)
{
_historyService = runHistoryService;
_telemetryService = telemetryService;
Id = "com.microsoft.cmdpal.builtin.run";
DisplayName = Resources.cmd_plugin_name;
Icon = Icons.RunV2Icon;
Settings = _settingsManager.Settings;
_shellListPage = new ShellListPage(_settingsManager, _historyService);
_shellListPage = new ShellListPage(_settingsManager, _historyService, _telemetryService);
_fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory);
_fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory, _telemetryService);
_shellPageItem = new CommandItem(_shellListPage)
{

View File

@@ -159,15 +159,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
internal static string Page_Name {
get {
return ResourceManager.GetString("Page_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>

View File

@@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Activation
{
// For more information on understanding and extending activation flow see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md
internal abstract class ActivationHandler
{
public abstract bool CanHandle(object args);
public abstract Task HandleAsync(object args);
}
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")]
internal abstract class ActivationHandler<T> : ActivationHandler
where T : class
{
public override async Task HandleAsync(object args)
{
await HandleInternalAsync(args as T).ConfigureAwait(false);
}
public override bool CanHandle(object args)
{
// CanHandle checks the args is of type you have configured
return args is T && CanHandleInternal(args as T);
}
// Override this method to add the activation logic in your activation handler
protected abstract Task HandleInternalAsync(T args);
// You can override this method to add extra validation on activation args
// to determine if your ActivationHandler should handle this activation args
protected virtual bool CanHandleInternal(T args)
{
return true;
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Services;
using Windows.ApplicationModel.Activation;
namespace Microsoft.PowerToys.Settings.UI.Activation
{
internal sealed class DefaultActivationHandler : ActivationHandler<IActivatedEventArgs>
{
private readonly Type navElement;
public DefaultActivationHandler(Type navElement)
{
this.navElement = navElement;
}
protected override async Task HandleInternalAsync(IActivatedEventArgs args)
{
// When the navigation stack isn't restored, navigate to the first page and configure
// the new page by passing required information in the navigation parameter
object arguments = null;
if (args is LaunchActivatedEventArgs launchArgs)
{
arguments = launchArgs.Arguments;
}
NavigationService.Navigate(navElement, arguments);
await Task.CompletedTask.ConfigureAwait(false);
}
protected override bool CanHandleInternal(IActivatedEventArgs args)
{
// None of the ActivationHandlers has handled the app activation
return NavigationService.Frame.Content == null && navElement != null;
}
}
}

View File

@@ -1,106 +0,0 @@
// Copyright (c) Microsoft Corporation
// The 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.PowerToys.Settings.UI.Activation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.Activation;
namespace Microsoft.PowerToys.Settings.UI.Services
{
// For more information on understanding and extending activation flow see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md
internal sealed class ActivationService
{
private readonly App app;
private readonly Type defaultNavItem;
private Lazy<UIElement> shell;
private object lastActivationArgs;
public ActivationService(App app, Type defaultNavItem, Lazy<UIElement> shell = null)
{
this.app = app;
this.shell = shell;
this.defaultNavItem = defaultNavItem;
}
public async Task ActivateAsync(object activationArgs)
{
if (IsInteractive(activationArgs))
{
// Initialize services that you need before app activation
// take into account that the splash screen is shown while this code runs.
await InitializeAsync().ConfigureAwait(false);
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (Window.Current.Content == null)
{
// Create a Shell or Frame to act as the navigation context
Window.Current.Content = shell?.Value ?? new Frame();
}
}
// Depending on activationArgs one of ActivationHandlers or DefaultActivationHandler
// will navigate to the first page
await HandleActivationAsync(activationArgs).ConfigureAwait(false);
lastActivationArgs = activationArgs;
if (IsInteractive(activationArgs))
{
// Ensure the current window is active
Window.Current.Activate();
// Tasks after activation
await StartupAsync().ConfigureAwait(false);
}
}
private static async Task InitializeAsync()
{
await Task.CompletedTask.ConfigureAwait(false);
}
private async Task HandleActivationAsync(object activationArgs)
{
var activationHandler = GetActivationHandlers()
.FirstOrDefault(h => h.CanHandle(activationArgs));
if (activationHandler != null)
{
await activationHandler.HandleAsync(activationArgs).ConfigureAwait(false);
}
if (IsInteractive(activationArgs))
{
var defaultHandler = new DefaultActivationHandler(defaultNavItem);
if (defaultHandler.CanHandle(activationArgs))
{
await defaultHandler.HandleAsync(activationArgs).ConfigureAwait(false);
}
}
}
private static async Task StartupAsync()
{
await Task.CompletedTask.ConfigureAwait(false);
}
private static IEnumerable<ActivationHandler> GetActivationHandlers()
{
yield break;
}
private static bool IsInteractive(object args)
{
return args is IActivatedEventArgs;
}
}
}

View File

@@ -24,12 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Services
{
get
{
if (frame == null)
{
frame = Window.Current.Content as Frame;
RegisterFrameEvents();
}
return frame;
}