Last active
September 22, 2024 09:21
-
-
Save hlaueriksson/5484a19def85f618d7a2297628486c80 to your computer and use it in GitHub Desktop.
2024-02-13-creating-custom-powertoys-run-plugins
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net8.0-windows</TargetFramework> | |
<Platforms>x64;ARM64</Platforms> | |
<PlatformTarget>$(Platform)</PlatformTarget> | |
<UseWPF>true</UseWPF> | |
</PropertyGroup> | |
<PropertyGroup> | |
<LangVersion>preview</LangVersion> | |
<ImplicitUsings>enable</ImplicitUsings> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<ItemGroup Condition="'$(Platform)' == 'x64'"> | |
<Reference Include="..\libs\x64\PowerToys.Common.UI.dll" /> | |
<Reference Include="..\libs\x64\PowerToys.ManagedCommon.dll" /> | |
<Reference Include="..\libs\x64\PowerToys.Settings.UI.Lib.dll" /> | |
<Reference Include="..\libs\x64\Wox.Infrastructure.dll" /> | |
<Reference Include="..\libs\x64\Wox.Plugin.dll" /> | |
</ItemGroup> | |
<ItemGroup Condition="'$(Platform)' == 'ARM64'"> | |
<Reference Include="..\libs\ARM64\PowerToys.Common.UI.dll" /> | |
<Reference Include="..\libs\ARM64\PowerToys.ManagedCommon.dll" /> | |
<Reference Include="..\libs\ARM64\PowerToys.Settings.UI.Lib.dll" /> | |
<Reference Include="..\libs\ARM64\Wox.Infrastructure.dll" /> | |
<Reference Include="..\libs\ARM64\Wox.Plugin.dll" /> | |
</ItemGroup> | |
<ItemGroup> | |
<None Include="plugin.json"> | |
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |
</None> | |
<None Include="Images\*.png"> | |
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |
</None> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net8.0-windows</TargetFramework> | |
<Platforms>x64;ARM64</Platforms> | |
<PlatformTarget>$(Platform)</PlatformTarget> | |
<UseWPF>true</UseWPF> | |
</PropertyGroup> | |
<PropertyGroup> | |
<LangVersion>preview</LangVersion> | |
<ImplicitUsings>enable</ImplicitUsings> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Community.PowerToys.Run.Plugin.Dependencies" Version="0.84.1" /> | |
</ItemGroup> | |
<ItemGroup> | |
<None Include="plugin.json"> | |
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |
</None> | |
<None Include="Images\*.png"> | |
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |
</None> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net8.0-windows</TargetFramework> | |
<Platforms>x64;ARM64</Platforms> | |
<PlatformTarget>$(Platform)</PlatformTarget> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> | |
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" /> | |
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> | |
<PackageReference Include="NLog" Version="5.0.4" /> | |
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" /> | |
</ItemGroup> | |
<ItemGroup> | |
<ProjectReference Include="..\Community.PowerToys.Run.Plugin.Demo\Community.PowerToys.Run.Plugin.Demo.csproj" /> | |
</ItemGroup> | |
<ItemGroup Condition="'$(Platform)' == 'x64'"> | |
<Reference Include="..\libs\x64\Wox.Plugin.dll" /> | |
<Reference Include="..\libs\x64\PowerToys.Settings.UI.Lib.dll" /> | |
</ItemGroup> | |
<ItemGroup Condition="'$(Platform)' == 'ARM64'"> | |
<Reference Include="..\libs\ARM64\Wox.Plugin.dll" /> | |
<Reference Include="..\libs\ARM64\PowerToys.Settings.UI.Lib.dll" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Input; | |
using ManagedCommon; | |
using Microsoft.PowerToys.Settings.UI.Library; | |
using Wox.Plugin; | |
using Wox.Plugin.Logger; | |
namespace Community.PowerToys.Run.Plugin.Demo | |
{ | |
/// <summary> | |
/// Main class of this plugin that implement all used interfaces. | |
/// </summary> | |
public class Main : IPlugin, IContextMenu, ISettingProvider, IDisposable | |
{ | |
/// <summary> | |
/// ID of the plugin. | |
/// </summary> | |
public static string PluginID => "AE953C974C2241878F282EA18A7769E4"; | |
/// <summary> | |
/// Name of the plugin. | |
/// </summary> | |
public string Name => "Demo"; | |
/// <summary> | |
/// Description of the plugin. | |
/// </summary> | |
public string Description => "Count words and characters in text"; | |
/// <summary> | |
/// Additional options for the plugin. | |
/// </summary> | |
public IEnumerable<PluginAdditionalOption> AdditionalOptions => [ | |
new() | |
{ | |
Key = nameof(CountSpaces), | |
DisplayLabel = "Count spaces", | |
DisplayDescription = "Count spaces as characters", | |
PluginOptionType = PluginAdditionalOption.AdditionalOptionType.Checkbox, | |
Value = CountSpaces, | |
} | |
]; | |
private bool CountSpaces { get; set; } | |
private PluginInitContext? Context { get; set; } | |
private string? IconPath { get; set; } | |
private bool Disposed { get; set; } | |
/// <summary> | |
/// Return a filtered list, based on the given query. | |
/// </summary> | |
/// <param name="query">The query to filter the list.</param> | |
/// <returns>A filtered list, can be empty when nothing was found.</returns> | |
public List<Result> Query(Query query) | |
{ | |
Log.Info("Query: " + query.Search, GetType()); | |
var words = query.Terms.Count; | |
// Average rate for transcription: 32.5 words per minute | |
// https://en.wikipedia.org/wiki/Words_per_minute | |
var transcription = TimeSpan.FromMinutes(words / 32.5); | |
var minutes = $"{(int)transcription.TotalMinutes}:{transcription.Seconds:00}"; | |
var charactersWithSpaces = query.Search.Length; | |
var charactersWithoutSpaces = query.Terms.Sum(x => x.Length); | |
return [ | |
new() | |
{ | |
QueryTextDisplay = query.Search, | |
IcoPath = IconPath, | |
Title = $"Words: {words}", | |
SubTitle = $"Transcription: {minutes} minutes", | |
ToolTipData = new ToolTipData("Words", $"{words} words\n{minutes} minutes for transcription\nAverage rate for transcription: 32.5 words per minute"), | |
ContextData = (words, transcription), | |
}, | |
new() | |
{ | |
QueryTextDisplay = query.Search, | |
IcoPath = IconPath, | |
Title = $"Characters: {(CountSpaces ? charactersWithSpaces : charactersWithoutSpaces)}", | |
SubTitle = CountSpaces ? "With spaces" : "Without spaces", | |
ToolTipData = new ToolTipData("Characters", $"{charactersWithSpaces} characters (with spaces)\n{charactersWithoutSpaces} characters (without spaces)"), | |
ContextData = CountSpaces ? charactersWithSpaces : charactersWithoutSpaces, | |
}, | |
]; | |
} | |
/// <summary> | |
/// Initialize the plugin with the given <see cref="PluginInitContext"/>. | |
/// </summary> | |
/// <param name="context">The <see cref="PluginInitContext"/> for this plugin.</param> | |
public void Init(PluginInitContext context) | |
{ | |
Log.Info("Init", GetType()); | |
Context = context ?? throw new ArgumentNullException(nameof(context)); | |
Context.API.ThemeChanged += OnThemeChanged; | |
UpdateIconPath(Context.API.GetCurrentTheme()); | |
} | |
/// <summary> | |
/// Return a list context menu entries for a given <see cref="Result"/> (shown at the right side of the result). | |
/// </summary> | |
/// <param name="selectedResult">The <see cref="Result"/> for the list with context menu entries.</param> | |
/// <returns>A list context menu entries.</returns> | |
public List<ContextMenuResult> LoadContextMenus(Result selectedResult) | |
{ | |
Log.Info("LoadContextMenus", GetType()); | |
if (selectedResult?.ContextData is (int words, TimeSpan transcription)) | |
{ | |
return | |
[ | |
new ContextMenuResult | |
{ | |
PluginName = Name, | |
Title = "Copy (Enter)", | |
FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", | |
Glyph = "\xE8C8", // Copy | |
AcceleratorKey = Key.Enter, | |
Action = _ => CopyToClipboard(words.ToString()), | |
}, | |
new ContextMenuResult | |
{ | |
PluginName = Name, | |
Title = "Copy time (Ctrl+Enter)", | |
FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", | |
Glyph = "\xE916", // Stopwatch | |
AcceleratorKey = Key.Enter, | |
AcceleratorModifiers = ModifierKeys.Control, | |
Action = _ => CopyToClipboard(transcription.ToString()), | |
}, | |
]; | |
} | |
if (selectedResult?.ContextData is int characters) | |
{ | |
return | |
[ | |
new ContextMenuResult | |
{ | |
PluginName = Name, | |
Title = "Copy (Enter)", | |
FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", | |
Glyph = "\xE8C8", // Copy | |
AcceleratorKey = Key.Enter, | |
Action = _ => CopyToClipboard(characters.ToString()), | |
}, | |
]; | |
} | |
return []; | |
} | |
/// <summary> | |
/// Creates setting panel. | |
/// </summary> | |
/// <returns>The control.</returns> | |
/// <exception cref="NotImplementedException">method is not implemented.</exception> | |
public Control CreateSettingPanel() => throw new NotImplementedException(); | |
/// <summary> | |
/// Updates settings. | |
/// </summary> | |
/// <param name="settings">The plugin settings.</param> | |
public void UpdateSettings(PowerLauncherPluginSettings settings) | |
{ | |
Log.Info("UpdateSettings", GetType()); | |
CountSpaces = settings.AdditionalOptions.SingleOrDefault(x => x.Key == nameof(CountSpaces))?.Value ?? false; | |
} | |
/// <inheritdoc/> | |
public void Dispose() | |
{ | |
Log.Info("Dispose", GetType()); | |
Dispose(true); | |
GC.SuppressFinalize(this); | |
} | |
/// <summary> | |
/// Wrapper method for <see cref="Dispose()"/> that dispose additional objects and events form the plugin itself. | |
/// </summary> | |
/// <param name="disposing">Indicate that the plugin is disposed.</param> | |
protected virtual void Dispose(bool disposing) | |
{ | |
if (Disposed || !disposing) | |
{ | |
return; | |
} | |
if (Context?.API != null) | |
{ | |
Context.API.ThemeChanged -= OnThemeChanged; | |
} | |
Disposed = true; | |
} | |
private void UpdateIconPath(Theme theme) => IconPath = theme == Theme.Light || theme == Theme.HighContrastWhite ? Context?.CurrentPluginMetadata.IcoPathLight : Context?.CurrentPluginMetadata.IcoPathDark; | |
private void OnThemeChanged(Theme currentTheme, Theme newTheme) => UpdateIconPath(newTheme); | |
private static bool CopyToClipboard(string? value) | |
{ | |
if (value != null) | |
{ | |
Clipboard.SetText(value); | |
} | |
return true; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace Community.PowerToys.Run.Plugin.Demo.UnitTests | |
{ | |
[TestClass] | |
public class MainTests | |
{ | |
private Main _subject = null!; | |
[TestInitialize] | |
public void TestInitialize() | |
{ | |
_subject = new Main(); | |
} | |
[TestMethod] | |
public void Query_should_calculate_the_number_of_words() | |
{ | |
var results = _subject.Query(new("")); | |
Assert.AreEqual("Words: 0", results[0].Title); | |
results = _subject.Query(new("Hello World")); | |
Assert.AreEqual("Words: 2", results[0].Title); | |
} | |
[TestMethod] | |
public void Query_should_calculate_the_number_of_characters() | |
{ | |
var results = _subject.Query(new("")); | |
Assert.AreEqual("Characters: 0", results[1].Title); | |
results = _subject.Query(new("Hello World")); | |
Assert.AreEqual("Characters: 10", results[1].Title); | |
} | |
[TestMethod] | |
public void LoadContextMenus_should_return_buttons_for_words_result() | |
{ | |
var results = _subject.LoadContextMenus(new() { ContextData = (2, TimeSpan.FromSeconds(3)) }); | |
Assert.AreEqual(2, results.Count); | |
Assert.AreEqual("Copy (Enter)", results[0].Title); | |
Assert.AreEqual("Copy time (Ctrl+Enter)", results[1].Title); | |
} | |
[TestMethod] | |
public void LoadContextMenus_should_return_button_for_characters_result() | |
{ | |
var results = _subject.LoadContextMenus(new() { ContextData = 10 }); | |
Assert.AreEqual(1, results.Count); | |
Assert.AreEqual("Copy (Enter)", results[0].Title); | |
} | |
[TestMethod] | |
public void AdditionalOptions_should_return_option_for_CountSpaces() | |
{ | |
var options = _subject.AdditionalOptions; | |
Assert.AreEqual(1, options.Count()); | |
Assert.AreEqual("CountSpaces", options.ElementAt(0).Key); | |
Assert.AreEqual(false, options.ElementAt(0).Value); | |
} | |
[TestMethod] | |
public void UpdateSettings_should_set_CountSpaces() | |
{ | |
_subject.UpdateSettings(new() { AdditionalOptions = [new() { Key = "CountSpaces", Value = true }] }); | |
var results = _subject.Query(new("Hello World")); | |
Assert.AreEqual("Characters: 11", results[1].Title); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"ID": "AE953C974C2241878F282EA18A7769E4", | |
"ActionKeyword": "demo", | |
"IsGlobal": false, | |
"Name": "Demo", | |
"Author": "hlaueriksson", | |
"Version": "1.0.0", | |
"Language": "csharp", | |
"Website": "https://github.com/hlaueriksson/ConductOfCode", | |
"ExecuteFileName": "Community.PowerToys.Run.Plugin.Demo.dll", | |
"IcoPathDark": "Images\\demo.dark.png", | |
"IcoPathLight": "Images\\demo.light.png", | |
"DynamicLoading": false | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment