Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save hlaueriksson/5484a19def85f618d7a2297628486c80 to your computer and use it in GitHub Desktop.
Save hlaueriksson/5484a19def85f618d7a2297628486c80 to your computer and use it in GitHub Desktop.
2024-02-13-creating-custom-powertoys-run-plugins
<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>
<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>
<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>
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;
}
}
}
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);
}
}
}
{
"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