Compare commits

...

1 Commits

Author SHA1 Message Date
vanzue
4e4d0a610f wire things up for cmdpal & kbm 2026-03-12 18:11:41 +08:00
35 changed files with 2645 additions and 75 deletions

View File

@@ -0,0 +1,308 @@
# Keyboard Manager CmdPal Integration
## Goal
Expose Keyboard Manager mappings in Command Palette (`cmdpal`) through `ext.powertoys` with two separate user experiences:
- quick actions for executable mappings
- an inspection list for all current mappings
This should be done without introducing a new settings schema or a CmdPal-specific Keyboard Manager settings file.
The first scope should cover:
- Expose `Run Program` remaps as invokable CmdPal actions
- Expose `Open URI` remaps as invokable CmdPal actions
- Add one Keyboard Manager `List all mappings` command item
- List all current mappings on a dedicated Keyboard Manager page
- Make the primary interaction for a mapping item be inspection of what that mapping does
## Current State
The repository already contains most of the plumbing needed for this integration:
1. Keyboard Manager publishes actions through the module action surface in `src/modules/keyboardmanager/dll/dllmain.cpp`.
2. Runner aggregates module actions through `src/runner/action_registry.cpp` and exposes them over the existing named pipe consumed by `RunnerActionClient`.
3. The PowerToys CmdPal extension enumerates module commands through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs`.
4. Keyboard Manager already has a provider in `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs` that:
- shows the active-state toggle
- opens the new editor
- enumerates Runner actions with the `powertoys.keyboardManager.mapping.` prefix
- invokes them through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/InvokeKeyboardManagerCustomActionCommand.cs`
At the Keyboard Manager layer, only remappings backed by executable actions are currently turned into invokable actions:
- `Shortcut::IsRunProgram()`
- `Shortcut::IsOpenURI()`
This is implemented in `is_keyboard_manager_custom_action`, `append_mapping_actions`, and `invoke_keyboard_manager_custom_action` in `src/modules/keyboardmanager/dll/dllmain.cpp`.
The repository also already has read-side Keyboard Manager mapping logic in `src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs`, but that code lives inside the editor UI project and is not an appropriate dependency for `ext.powertoys`.
## Lightweight Design
### Design Decision
Split the design into two surfaces:
1. Executable mapping actions
2. Mapping inspection
Executable mapping actions stay centered on the existing Runner action registry.
Mapping inspection should use a dedicated read-only Keyboard Manager query service shared with CmdPal, following the existing `*.ModuleServices` pattern used by other modules.
Do not add:
- a new CmdPal-only data file
- a direct JSON parse of Keyboard Manager settings in `ext.powertoys`
- a UI-project dependency from `ext.powertoys` to `KeyboardManagerEditorUI`
- CmdPal-specific logic in the Keyboard Manager editor
### Why This Is The Right Shape
This matches the way other PowerToys modules integrate with CmdPal:
- module owns its state and execution semantics
- executable actions are published through a small action surface
- richer read-only data can be exposed through a shared service layer when the module needs inspection or navigation
This keeps the startup path lean and avoids duplicating Keyboard Manager parsing logic in `ext.powertoys`.
## Proposed Functional Model
### Surface A: Executable Actions
Keyboard Manager remains the source of truth for which mappings are eligible for direct invocation in CmdPal.
When `get_actions()` is called:
1. Load the current `MappingConfiguration`
2. Enumerate OS-level shortcut remaps
3. Enumerate app-specific shortcut remaps
4. Keep only remaps whose target operation is:
- `Run Program`
- `Open URI`
5. Emit one Runner action descriptor per eligible remap
### Identity For Executable Actions
Each action id remains derived from the remap identity:
- source shortcut
- exact-match flag
- app scope
- target operation type
- target payload fields
The current implementation uses a hashed identity under the prefix `powertoys.keyboardManager.mapping.`. That is acceptable for a lightweight design because:
- CmdPal does not need stable ids across edits beyond the current session
- the action id is regenerated from source-of-truth settings
- action invocation already re-resolves the action against current config and fails safely if the mapping no longer exists
### Invocation Of Executable Actions
CmdPal invokes the selected item through `RunnerActionClient.InvokeAction(actionId)`.
Keyboard Manager stays responsible for:
- launching programs
- open-existing-instance behavior
- elevation mode
- start-in directory
- window visibility
- URI/path normalization and shell execution
This is important because CmdPal should not duplicate Keyboard Manager's execution semantics.
### Surface B: All Mappings Inspection
CmdPal also needs one dedicated Keyboard Manager entry for inspecting every current mapping, not just executable ones.
That entry should be a top-level module item such as:
- `List Keyboard Manager mappings`
Its command should open a dedicated `KeyboardManagerMappingsPage`.
### Data Source For All Mappings
The `KeyboardManagerMappingsPage` should not be backed by Runner actions because Runner actions currently model invokable operations only.
Instead, add a small shared Keyboard Manager query layer, ideally as a module service project, for example:
- `src/modules/keyboardmanager/KeyboardManager.ModuleServices`
That shared service should reuse the existing native mapping query path already used by the editor and expose normalized read-only DTOs for CmdPal consumption.
The service should cover all current mapping categories:
- single key to key
- single key to shortcut
- single key to text
- shortcut to shortcut
- shortcut to program
- shortcut to URI
- app-specific shortcut mappings
### Interaction Model For The Mappings Page
The mappings page should behave like an inspection page first, not an execution page first.
Recommended interaction:
1. `KeyboardManagerMappingsPage` is a `DynamicListPage` or `ListPage` with `ShowDetails = true`
2. Each mapping is rendered as a `ListItem` with rich `Details`
3. Selecting a mapping shows what it maps to
4. Invoking the item opens a small `KeyboardManagerMappingDetailsPage` or equivalent detail-focused page
5. Executable mappings may expose an extra command such as `Run now` or `Open now`, but that should not be the primary action on the inspection page
This satisfies the requirement that the primary action for a mapping entry is to show what the mapping is, while still leaving room for execution when the mapping type supports it.
### Presentation In CmdPal
Within `ext.powertoys`, the Keyboard Manager provider should emit these command groups:
1. Keyboard Manager state commands
- toggle active state
- open editor
2. Keyboard Manager inspection commands
- `List Keyboard Manager mappings`
3. Keyboard Manager quick actions
- `Run Program` entries
- `Open URI` entries
4. Keyboard Manager settings
- open settings
## UX Guidance
The minimum viable experience is:
- searchable by trigger or target
- clearly labeled as Keyboard Manager actions or mappings
- capable of both inspection and direct execution for supported mapping types
Recommended presentation rules:
1. The `List Keyboard Manager mappings` item should use the Keyboard Manager icon and clearly signal it opens a list, not an action.
2. Mapping list titles should prioritize the trigger:
- `Ctrl+Alt+N`
- `Caps Lock`
3. Mapping list subtitles should say what the trigger maps to:
- `Opens notepad.exe`
- `Maps to Ctrl+C`
- `Types Hello world`
4. Mapping details should carry the rest of the context:
- global vs app-specific
- mapping kind
- target payload
- execution-specific options when relevant
5. Quick-action titles can continue to prioritize the executable action:
- `Run notepad.exe`
- `Open https://contoso.com`
6. Keyboard Manager module icon is sufficient for the first version
The current implementation already covers the quick-action portion. The new work is primarily the all-mappings inspection surface.
## Non-Goals For The First Version
Do not add these in the initial pass:
- editing Keyboard Manager mappings from CmdPal
- enabling or disabling individual mappings from CmdPal
- live push notifications when mappings change
- custom icons per program or URI
- a new `kbm:` command syntax or dedicated parser
These all increase complexity without being necessary to validate the feature.
## Integration Pattern Compared To Other Modules
This feature now combines two existing CmdPal integration styles.
- `Workspaces` loads module-owned data and emits one command per data item
- `FancyZones` uses dedicated pages and details for richer inspection
Keyboard Manager quick actions should follow the lighter `Workspaces` pattern:
- one provider
- one flat list of dynamic items
- generic command invocation
Keyboard Manager all-mappings inspection should follow the `list page with details` pattern already supported by CmdPal:
- one top-level entry that opens a page
- one list item per mapping
- rich `Details` on every row
- optional secondary commands for invokable mappings
The main constraint is the same in both paths: `ext.powertoys` should not duplicate Keyboard Manager's mapping schema by parsing the settings file directly.
## Error Handling
The existing executable-action behavior is the correct baseline:
- hidden or deleted mappings simply disappear from `list_actions`
- stale CmdPal entries fail through `action_not_found`
- disabled Keyboard Manager returns `module_unavailable`
- launch failures return module-defined error messages
CmdPal only needs to surface the returned message as toast text for quick actions.
For the all-mappings inspection page:
- malformed or unreadable mapping snapshots should yield an empty page or an inline error item
- missing targets should still render as mappings, but be tagged as invalid or unavailable
- details rendering should degrade gracefully when optional fields are absent
## Risks
### Two Data Paths
This design intentionally uses two integration paths:
- Runner actions for invokable mappings
- a shared read-only query service for all mappings
That is acceptable because the two paths serve different UX needs. The risk is manageable as long as both paths derive from the same Keyboard Manager mapping model rather than separate ad hoc parsers.
### Duplicate Or Ambiguous Entries
Different mappings may produce similar titles, especially on the quick-action side. This is acceptable in the first iteration because the subtitle already carries scope and trigger details.
### Action Id Churn After Edits
Editing a mapping changes the derived id. This is acceptable because action ids are not a persisted public contract.
### Large Mapping Sets
Very large mapping sets could make the inspection page noisy. This is manageable for the first version if the page supports search and details, but sectioning or filters may be needed later.
## Minimal Implementation Plan
1. Keep Keyboard Manager as the producer of invokable mapping actions.
2. Keep Runner action registry as the discovery path for executable quick actions.
3. Add a small shared read-only Keyboard Manager module service for enumerating all mappings.
4. Add `List Keyboard Manager mappings` to `KeyboardManagerModuleCommandProvider`.
5. Add a `KeyboardManagerMappingsPage` with `ShowDetails = true`.
6. Represent each mapping as an inspection-first `ListItem` with rich `Details`.
7. Add optional secondary execution commands only for mappings that are invokable.
8. Add focused tests around:
- Keyboard Manager action enumeration for `Run Program` and `Open URI`
- mapping snapshot enumeration across all mapping kinds
- CmdPal rendering of the mappings page
- graceful handling of stale, invalid, or missing mappings
## Future Extensions
If the first version lands well, the next step should still preserve the same split architecture:
- enrich action descriptors with more metadata if Runner actions grow argument or icon support
- add sections or filters to the mappings page when the list becomes large
- optionally expose app-specific filtering in CmdPal UI
The extension points should remain:
- Runner actions for execution
- a shared Keyboard Manager query service for inspection

View File

@@ -0,0 +1,193 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using PowerToys.Interop;
namespace ManagedCommon
{
public sealed class RunnerActionClient
{
public IReadOnlyList<RunnerActionDescriptor> ListActions()
{
var response = SendRequest("list_actions", string.Empty, "{}");
return response.Success && response.Actions.Count > 0 ? response.Actions : Array.Empty<RunnerActionDescriptor>();
}
public RunnerActionInvokeResult InvokeAction(string actionId, string serializedArguments = "{}")
{
if (string.IsNullOrWhiteSpace(actionId))
{
return new RunnerActionInvokeResult
{
Success = false,
ErrorCode = "invalid_action_id",
Message = "Action id is required.",
};
}
var response = SendRequest("invoke_action", actionId, string.IsNullOrWhiteSpace(serializedArguments) ? "{}" : serializedArguments);
return new RunnerActionInvokeResult
{
Success = response.Success,
ErrorCode = response.ErrorCode,
Message = response.Message,
};
}
private static RunnerActionResponse SendRequest(string requestType, string actionId, string arguments)
{
var pipeName = Path.GetFileName(Constants.PowerToysActionsPipe());
using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
pipe.Connect(2000);
var payload = BuildRequestPayload(requestType, actionId, arguments);
var lengthBuffer = BitConverter.GetBytes(payload.Length);
pipe.Write(lengthBuffer, 0, lengthBuffer.Length);
pipe.Write(payload, 0, payload.Length);
pipe.Flush();
var responseLengthBuffer = ReadExact(pipe, sizeof(int));
var responseLength = BitConverter.ToInt32(responseLengthBuffer, 0);
var responsePayload = responseLength == 0 ? Array.Empty<byte>() : ReadExact(pipe, responseLength);
return ParseResponse(responsePayload);
}
private static byte[] BuildRequestPayload(string requestType, string actionId, string arguments)
{
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartObject();
writer.WriteString("type", requestType);
if (!string.IsNullOrWhiteSpace(actionId))
{
writer.WriteString("action_id", actionId);
}
writer.WriteString("arguments", arguments);
writer.WriteEndObject();
}
return stream.ToArray();
}
private static RunnerActionResponse ParseResponse(byte[] payload)
{
if (payload.Length == 0)
{
return RunnerActionResponse.CreateError("empty_response", "Runner returned an empty response.");
}
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
var response = new RunnerActionResponse();
if (root.TryGetProperty("success", out var successElement) &&
(successElement.ValueKind == JsonValueKind.True || successElement.ValueKind == JsonValueKind.False))
{
response.Success = successElement.GetBoolean();
}
if (root.TryGetProperty("error_code", out var errorCodeElement) && errorCodeElement.ValueKind == JsonValueKind.String)
{
response.ErrorCode = errorCodeElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("message", out var messageElement) && messageElement.ValueKind == JsonValueKind.String)
{
response.Message = messageElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("actions", out var actionsElement) && actionsElement.ValueKind == JsonValueKind.Array)
{
response.Actions = ParseActions(actionsElement);
}
return response;
}
private static List<RunnerActionDescriptor> ParseActions(JsonElement actionsElement)
{
var actions = new List<RunnerActionDescriptor>();
foreach (var element in actionsElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object)
{
continue;
}
actions.Add(new RunnerActionDescriptor
{
ActionId = GetStringProperty(element, "action_id"),
ModuleKey = GetStringProperty(element, "module_key"),
DisplayName = GetStringProperty(element, "display_name"),
Description = GetStringProperty(element, "description"),
Category = GetStringProperty(element, "category"),
Available = GetBoolProperty(element, "available"),
});
}
return actions;
}
private static string GetStringProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
? property.GetString() ?? string.Empty
: string.Empty;
}
private static bool GetBoolProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) &&
(property.ValueKind == JsonValueKind.True || property.ValueKind == JsonValueKind.False) &&
property.GetBoolean();
}
private static byte[] ReadExact(Stream stream, int length)
{
var buffer = new byte[length];
var offset = 0;
while (offset < length)
{
var bytesRead = stream.Read(buffer, offset, length - offset);
if (bytesRead == 0)
{
throw new EndOfStreamException("Unexpected end of stream while reading runner action response.");
}
offset += bytesRead;
}
return buffer;
}
private sealed class RunnerActionResponse
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public List<RunnerActionDescriptor> Actions { get; set; } = new();
public static RunnerActionResponse CreateError(string errorCode, string message)
{
return new RunnerActionResponse
{
Success = false,
ErrorCode = errorCode,
Message = message,
};
}
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ManagedCommon
{
public sealed class RunnerActionDescriptor
{
public string ActionId { get; set; } = string.Empty;
public string ModuleKey { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool Available { get; set; }
}
}

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 ManagedCommon
{
public static class RunnerActionIds
{
public const string KeyboardManagerToggleActive = "powertoys.keyboardManager.toggleActive";
public const string KeyboardManagerOpenEditor = "powertoys.keyboardManager.openEditor";
}
}

View File

@@ -0,0 +1,15 @@
// 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 ManagedCommon
{
public sealed class RunnerActionInvokeResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
}

View File

@@ -308,5 +308,9 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
}
hstring Constants::PowerToysActionsPipe()
{
return CommonSharedConstants::POWERTOYS_ACTIONS_PIPE;
}
}

View File

@@ -80,6 +80,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring OpenNewKeyboardManagerEvent();
static hstring ToggleKeyboardManagerActiveEvent();
static hstring KeyboardManagerEngineInstanceMutex();
static hstring PowerToysActionsPipe();
};
}

View File

@@ -77,6 +77,7 @@ namespace PowerToys
static String OpenNewKeyboardManagerEvent();
static String ToggleKeyboardManagerActiveEvent();
static String KeyboardManagerEngineInstanceMutex();
static String PowerToysActionsPipe();
}
}
}

View File

@@ -174,6 +174,7 @@ namespace CommonSharedConstants
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
const wchar_t POWERTOYS_ACTIONS_PIPE[] = L"\\\\.\\pipe\\PowerToysActionsPipe-e98c2e3d-52ab-4f9b-a65b-5b9bb6f0e312";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Commands;
internal sealed partial class InvokeKeyboardManagerCustomActionCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
private readonly string _actionId;
private readonly string _fallbackError;
public InvokeKeyboardManagerCustomActionCommand(string actionId, string displayName)
{
_actionId = actionId;
_fallbackError = $"Failed to invoke {displayName}.";
Name = displayName;
}
public override CommandResult Invoke()
{
try
{
var result = ActionClient.InvokeAction(_actionId);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? _fallbackError : result.Message);
}
catch (Exception ex)
{
return CommandResult.ShowToast($"{_fallbackError} {ex.Message}");
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
namespace PowerToysExtension.Commands;
internal sealed partial class KeyboardManagerMappingListItem : ListItem
{
public KeyboardManagerMappingListItem(KeyboardManagerMappingRecord mapping, IconInfo icon)
: base(new CommandItem(new KeyboardManagerMappingDetailsPage(mapping, icon)))
{
Title = mapping.TriggerDisplay;
Subtitle = mapping.Subtitle;
Icon = icon;
Details = BuildDetails(mapping, icon);
}
private static Details BuildDetails(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Target", mapping.TargetDisplay),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
return new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = metadata.ToArray(),
};
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
}

View File

@@ -3,9 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
@@ -14,6 +13,8 @@ namespace PowerToysExtension.Commands;
/// </summary>
internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
public OpenNewKeyboardManagerEditorCommand()
{
Name = "Open New Keyboard Manager Editor";
@@ -23,9 +24,10 @@ internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCom
{
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent());
evt.Set();
return CommandResult.Dismiss();
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerOpenEditor);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? "Failed to open New Keyboard Manager Editor." : result.Message);
}
catch (Exception ex)
{

View File

@@ -4,6 +4,7 @@
using System;
using System.Threading;
using ManagedCommon;
using PowerToys.Interop;
namespace PowerToysExtension.Helpers;
@@ -12,6 +13,7 @@ internal static class KeyboardManagerStateService
{
private static readonly object Sync = new();
private static readonly Timer PollingTimer;
private static readonly RunnerActionClient ActionClient = new();
private static bool _lastKnownListeningState = IsListening();
internal static event Action? StatusChanged;
@@ -47,10 +49,9 @@ internal static class KeyboardManagerStateService
{
try
{
using var evt = EventWaitHandle.OpenExisting(Constants.ToggleKeyboardManagerActiveEvent());
var signaled = evt.Set();
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerToggleActive);
PollStatus();
return signaled;
return result.Success;
}
catch
{

View File

@@ -63,6 +63,7 @@
<ProjectReference Include="..\..\..\Awake\Awake.ModuleServices\Awake.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\colorPicker\ColorPicker.ModuleServices\ColorPicker.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\..\..\keyboardmanager\KeyboardManager.ModuleServices\KeyboardManager.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\Workspaces\Workspaces.ModuleServices\Workspaces.ModuleServices.csproj" />
</ItemGroup>

View File

@@ -5,10 +5,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
@@ -16,6 +19,9 @@ namespace PowerToysExtension.Modules;
internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvider
{
private const string KeyboardManagerMappingActionPrefix = "powertoys.keyboardManager.mapping.";
private static readonly RunnerActionClient ActionClient = new();
public override IEnumerable<ListItem> BuildCommands()
{
var module = SettingsWindow.KBM;
@@ -33,9 +39,16 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
: GetResourceString("KeyboardManager_ToggleListening_Off_Subtitle", "Keyboard Manager is paused. Invoke to start listening."),
Icon = PowerToysResourcesHelper.KeyboardManagerListeningIcon(isListening),
};
yield return new ListItem(new CommandItem(new KeyboardManagerMappingsPage() { Id = "com.microsoft.powertoys.keyboardManager.mappings" }))
{
Title = "List Keyboard Manager mappings",
Subtitle = "Inspect current remaps and shortcuts from Keyboard Manager.",
Icon = icon,
};
}
if (IsUseNewEditorEnabled())
if (ModuleEnablementService.IsModuleEnabled(module) && IsUseNewEditorEnabled())
{
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand())
{
@@ -45,6 +58,19 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
};
}
if (ModuleEnablementService.IsModuleEnabled(module))
{
foreach (var action in ListExecutableMappingActions())
{
yield return new ListItem(new InvokeKeyboardManagerCustomActionCommand(action.ActionId, action.DisplayName) { Id = $"com.microsoft.powertoys.keyboardManager.action.{action.ActionId}" })
{
Title = action.DisplayName,
Subtitle = string.IsNullOrWhiteSpace(action.Description) ? "Invoke a Keyboard Manager custom action." : action.Description,
Icon = icon,
};
}
}
yield return new ListItem(new OpenInSettingsCommand(module, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" })
{
Title = title,
@@ -58,6 +84,21 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
return Resources.ResourceManager.GetString(resourceName, Resources.Culture) ?? fallback;
}
private static IEnumerable<RunnerActionDescriptor> ListExecutableMappingActions()
{
try
{
return ActionClient.ListActions()
.Where(action => action.Available && action.ActionId.StartsWith(KeyboardManagerMappingActionPrefix, StringComparison.Ordinal))
.OrderBy(action => action.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
catch
{
return Array.Empty<RunnerActionDescriptor>();
}
}
private static bool IsUseNewEditorEnabled()
{
try

View File

@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingDetailsPage : ContentPage
{
private readonly KeyboardManagerMappingRecord _mapping;
public KeyboardManagerMappingDetailsPage(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
_mapping = mapping;
Icon = icon;
Name = mapping.TriggerDisplay;
Details = new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = BuildMetadata(mapping),
};
}
public override IContent[] GetContent()
{
return
[
new MarkdownContent(
$"""
# {EscapeMarkdown(_mapping.TriggerDisplay)}
{EscapeMarkdown(_mapping.Subtitle)}
"""),
];
}
private static IDetailsElement[] BuildMetadata(KeyboardManagerMappingRecord mapping)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
DetailText("Target", mapping.TargetDisplay),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramPath))
{
metadata.Add(DetailText("Program", mapping.ProgramPath));
}
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
return metadata.ToArray();
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
private static string EscapeMarkdown(string value)
{
return value.Replace("\\", "\\\\").Replace("*", "\\*").Replace("_", "\\_");
}
}

View File

@@ -0,0 +1,66 @@
// 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 KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingsPage : DynamicListPage
{
private readonly CommandItem _emptyMessage;
private readonly IconInfo _icon;
public KeyboardManagerMappingsPage()
{
_icon = PowerToysResourcesHelper.IconFromSettingsIcon("KeyboardManager.png");
Icon = _icon;
Name = Title = "Keyboard Manager mappings";
Id = "com.microsoft.cmdpal.powertoys.keyboardManager.mappings";
ShowDetails = true;
_emptyMessage = new CommandItem
{
Title = "No Keyboard Manager mappings found",
Subtitle = "Create mappings in Keyboard Manager to inspect them here.",
Icon = _icon,
};
EmptyContent = _emptyMessage;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
RaiseItemsChanged(0);
}
public override IListItem[] GetItems()
{
var result = KeyboardManagerMappingService.Instance.GetMappingsAsync().GetAwaiter().GetResult();
if (!result.Success || result.Value is null)
{
_emptyMessage.Subtitle = result.Error ?? "Failed to read Keyboard Manager mappings.";
return Array.Empty<IListItem>();
}
IEnumerable<KeyboardManagerMappingRecord> mappings = result.Value;
if (!string.IsNullOrWhiteSpace(SearchText))
{
mappings = mappings.Where(mapping =>
mapping.TriggerDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetApp.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase));
}
return mappings
.OrderBy(mapping => mapping.TriggerDisplay, StringComparer.CurrentCultureIgnoreCase)
.Select(mapping => (IListItem)new KeyboardManagerMappingListItem(mapping, _icon))
.ToArray();
}
}

View File

@@ -1,7 +1,8 @@
#pragma once
#include <compare>
#include <common/utils/gpo.h>
#pragma once
#include <compare>
#include <cwchar>
#include <common/utils/gpo.h>
/*
DLL Interface for PowerToys. The powertoy_create() (see below) must return
@@ -97,10 +98,53 @@ public:
virtual bool get_config(wchar_t* buffer, int* buffer_size) = 0;
/* Sets the configuration values. */
virtual void set_config(const wchar_t* config) = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Returns the actions exposed by the module as a JSON array. */
virtual bool get_actions(wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t empty_actions[] = L"[]";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(empty_actions) / sizeof(empty_actions[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, empty_actions);
return true;
}
/* Invokes an action exposed by the module and returns a JSON object result. */
virtual bool invoke_action(const wchar_t* /*action_id*/, const wchar_t* /*serialized_args*/, wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t unsupported_result[] =
L"{\"success\":false,\"error_code\":\"not_supported\",\"message\":\"This module does not expose actions.\"}";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(unsupported_result) / sizeof(unsupported_result[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, unsupported_result);
return true;
}
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Disables the PowerToy, should free as much memory as possible. */
virtual void disable() = 0;
/* Should return if the PowerToys is enabled or disabled. */

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
<ProjectReference Include="..\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Text;
namespace KeyboardManager.ModuleServices;
internal static class KeyboardManagerInterop
{
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
private const CallingConvention Convention = CallingConvention.Cdecl;
[DllImport(DllName, CallingConvention = Convention)]
internal static extern IntPtr CreateMappingConfiguration();
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void DestroyMappingConfiguration(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool LoadMappingSettings(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref NativeSingleKeyMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref NativeKeyboardTextMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetShortcutRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemap(IntPtr config, int index, ref NativeShortcutMapping mapping);
[DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void FreeString(IntPtr str);
internal static string GetStringAndFree(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
return string.Empty;
}
var result = Marshal.PtrToStringUni(handle) ?? string.Empty;
FreeString(handle);
return result;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeSingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;
[MarshalAs(UnmanagedType.Bool)]
public bool IsShortcut;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeKeyboardTextMapping
{
public int OriginalKey;
public IntPtr TargetText;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeShortcutMapping
{
public IntPtr OriginalKeys;
public IntPtr TargetKeys;
public IntPtr TargetApp;
public int OperationType;
public IntPtr TargetText;
public IntPtr ProgramPath;
public IntPtr ProgramArgs;
public IntPtr StartInDirectory;
public int Elevation;
public int IfRunningAction;
public int Visibility;
public IntPtr UriToOpen;
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace KeyboardManager.ModuleServices;
public enum KeyboardManagerMappingKind
{
SingleKeyToKey,
SingleKeyToShortcut,
SingleKeyToText,
ShortcutToKey,
ShortcutToShortcut,
ShortcutToText,
ShortcutToProgram,
ShortcutToUri,
}
public sealed class KeyboardManagerMappingRecord
{
public string Id { get; init; } = string.Empty;
public KeyboardManagerMappingKind Kind { get; init; }
public string TriggerDisplay { get; init; } = string.Empty;
public string TargetDisplay { get; init; } = string.Empty;
public string Subtitle { get; init; } = string.Empty;
public bool IsAppSpecific { get; init; }
public string TargetApp { get; init; } = string.Empty;
public string OriginalKeys { get; init; } = string.Empty;
public string TargetKeys { get; init; } = string.Empty;
public string TargetText { get; init; } = string.Empty;
public string ProgramPath { get; init; } = string.Empty;
public string ProgramArgs { get; init; } = string.Empty;
public string StartInDirectory { get; init; } = string.Empty;
public int Elevation { get; init; }
public int IfRunningAction { get; init; }
public int Visibility { get; init; }
public string UriToOpen { get; init; } = string.Empty;
public bool IsExecutable => Kind is KeyboardManagerMappingKind.ShortcutToProgram or KeyboardManagerMappingKind.ShortcutToUri;
}

View File

@@ -0,0 +1,236 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using ManagedCommon;
namespace KeyboardManager.ModuleServices;
public sealed class KeyboardManagerMappingService
{
public static KeyboardManagerMappingService Instance { get; } = new();
public Task<PowerToys.ModuleContracts.OperationResult<IReadOnlyList<KeyboardManagerMappingRecord>>> GetMappingsAsync(CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
using var session = new MappingConfigurationSession();
if (!KeyboardManagerInterop.LoadMappingSettings(session.Handle))
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Failed to load Keyboard Manager mappings."));
}
var mappings = new List<KeyboardManagerMappingRecord>();
LoadSingleKeyMappings(session.Handle, mappings);
LoadSingleKeyTextMappings(session.Handle, mappings);
LoadShortcutMappings(session.Handle, mappings);
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Ok<IReadOnlyList<KeyboardManagerMappingRecord>>(mappings));
}
catch (OperationCanceledException)
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Keyboard Manager mapping query was cancelled."));
}
catch (Exception ex)
{
Logger.LogError($"Failed to enumerate Keyboard Manager mappings: {ex.Message}");
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>($"Failed to enumerate Keyboard Manager mappings: {ex.Message}"));
}
}
private static void LoadSingleKeyMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeSingleKeyMapping);
if (!KeyboardManagerInterop.GetSingleKeyRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetRaw = KeyboardManagerInterop.GetStringAndFree(native.TargetKey);
var targetDisplay = native.IsShortcut ? FormatShortcut(targetRaw) : FormatSingleKey(targetRaw);
var kind = native.IsShortcut ? KeyboardManagerMappingKind.SingleKeyToShortcut : KeyboardManagerMappingKind.SingleKeyToKey;
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetRaw, native.IsShortcut.ToString()),
Kind = kind,
TriggerDisplay = originalDisplay,
TargetDisplay = targetDisplay,
Subtitle = $"Maps to {targetDisplay}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetKeys = targetRaw,
});
}
}
private static void LoadSingleKeyTextMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyToTextRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeKeyboardTextMapping);
if (!KeyboardManagerInterop.GetSingleKeyToTextRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key-text", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetText),
Kind = KeyboardManagerMappingKind.SingleKeyToText,
TriggerDisplay = originalDisplay,
TargetDisplay = targetText,
Subtitle = $"Types {targetText}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetText = targetText,
});
}
}
private static void LoadShortcutMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetShortcutRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeShortcutMapping);
if (!KeyboardManagerInterop.GetShortcutRemap(handle, i, ref native))
{
continue;
}
var originalKeys = KeyboardManagerInterop.GetStringAndFree(native.OriginalKeys);
var targetKeys = KeyboardManagerInterop.GetStringAndFree(native.TargetKeys);
var targetApp = KeyboardManagerInterop.GetStringAndFree(native.TargetApp);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
var programPath = KeyboardManagerInterop.GetStringAndFree(native.ProgramPath);
var programArgs = KeyboardManagerInterop.GetStringAndFree(native.ProgramArgs);
var startInDirectory = KeyboardManagerInterop.GetStringAndFree(native.StartInDirectory);
var uriToOpen = KeyboardManagerInterop.GetStringAndFree(native.UriToOpen);
var triggerDisplay = FormatShortcut(originalKeys);
var (kind, targetDisplay, subtitle) = DescribeShortcutTarget(native.OperationType, targetKeys, targetText, programPath, uriToOpen);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("shortcut", originalKeys, targetKeys, targetText, programPath, programArgs, startInDirectory, uriToOpen, targetApp, native.OperationType.ToString(CultureInfo.InvariantCulture)),
Kind = kind,
TriggerDisplay = triggerDisplay,
TargetDisplay = targetDisplay,
Subtitle = string.IsNullOrWhiteSpace(targetApp) ? subtitle : $"{subtitle} in {targetApp}",
IsAppSpecific = !string.IsNullOrWhiteSpace(targetApp),
TargetApp = targetApp,
OriginalKeys = originalKeys,
TargetKeys = targetKeys,
TargetText = targetText,
ProgramPath = programPath,
ProgramArgs = programArgs,
StartInDirectory = startInDirectory,
Elevation = native.Elevation,
IfRunningAction = native.IfRunningAction,
Visibility = native.Visibility,
UriToOpen = uriToOpen,
});
}
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutTarget(int operationType, string targetKeys, string targetText, string programPath, string uriToOpen)
{
return operationType switch
{
1 => (KeyboardManagerMappingKind.ShortcutToProgram, programPath, $"Opens {programPath}"),
2 => (KeyboardManagerMappingKind.ShortcutToUri, uriToOpen, $"Opens {uriToOpen}"),
3 => (KeyboardManagerMappingKind.ShortcutToText, targetText, $"Types {targetText}"),
_ => DescribeShortcutRemap(targetKeys),
};
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutRemap(string targetKeys)
{
var keyCodes = ParseKeyCodes(targetKeys);
if (keyCodes.Count <= 1)
{
var targetDisplay = FormatSingleKey(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToKey, targetDisplay, $"Maps to {targetDisplay}");
}
var shortcutDisplay = FormatShortcut(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToShortcut, shortcutDisplay, $"Maps to {shortcutDisplay}");
}
private static string FormatSingleKey(string value)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode))
{
return GetKeyDisplayName(keyCode);
}
return value;
}
private static string FormatShortcut(string value)
{
var parts = ParseKeyCodes(value)
.Select(GetKeyDisplayName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
return parts.Length == 0 ? value : string.Join(" + ", parts);
}
private static IReadOnlyList<int> ParseKeyCodes(string value)
{
return value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode) ? keyCode : (int?)null)
.Where(keyCode => keyCode.HasValue)
.Select(keyCode => keyCode!.Value)
.ToArray();
}
private static string GetKeyDisplayName(int keyCode)
{
var buffer = new StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, buffer, buffer.Capacity);
return buffer.ToString();
}
private static string CreateId(params string[] values)
{
var payload = string.Join("|", values);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hash[..8]);
}
private sealed partial class MappingConfigurationSession : IDisposable
{
public MappingConfigurationSession()
{
Handle = KeyboardManagerInterop.CreateMappingConfiguration();
if (Handle == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create Keyboard Manager mapping configuration.");
}
}
public IntPtr Handle { get; private set; }
public void Dispose()
{
if (Handle != IntPtr.Zero)
{
KeyboardManagerInterop.DestroyMappingConfiguration(Handle);
Handle = IntPtr.Zero;
}
}
}
}

View File

@@ -29,7 +29,7 @@
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<AdditionalIncludeDirectories>./;$(SolutionDir)src\modules\;$(SolutionDir)src\common\Display;$(SolutionDir)src\common\inc;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>./;$(RepoRoot)src\modules\;$(RepoRoot)src\common\Display;$(RepoRoot)src\common\inc;$(RepoRoot)src\common\Telemetry;$(RepoRoot)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
@@ -117,6 +117,6 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
</Target>
</Project>
</Project>

View File

@@ -335,6 +335,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -351,6 +355,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
@@ -359,6 +367,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
break;
@@ -367,6 +379,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
}
@@ -375,10 +391,14 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->operationType = 3;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -438,6 +458,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -453,6 +477,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
@@ -461,6 +489,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
@@ -469,6 +501,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
@@ -476,10 +512,14 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->operationType = 3;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -749,4 +789,4 @@ int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
}
return count;
}
}

View File

@@ -32,6 +32,10 @@ struct ShortcutMapping
wchar_t* targetText;
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* startInDirectory;
int elevation;
int ifRunningAction;
int visibility;
wchar_t* uriToOpen;
};

View File

@@ -33,11 +33,11 @@
<AdditionalIncludeDirectories>$(RepoRoot)src\;$(RepoRoot)src\modules;$(RepoRoot)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>Shcore.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="Generated Files\resource.h" />

View File

@@ -1,16 +1,21 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/resources.h>
#include "Generated Files/resource.h"
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <common/utils/winapi_error.h>
#include <keyboardmanager/dll/trace.h>
#include <shellapi.h>
#include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h>
#include <thread>
#include <atomic>
#include <common/utils/resources.h>
#include "Generated Files/resource.h"
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <keyboardmanager/common/MappingConfiguration.h>
#include <common/utils/winapi_error.h>
#include <keyboardmanager/dll/trace.h>
#include <shellapi.h>
#include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h>
#include <thread>
#include <atomic>
#include <TlHelp32.h>
#include <future>
#include <iomanip>
#include <sstream>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
@@ -37,11 +42,634 @@ namespace
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
}
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
const wchar_t ACTION_ID_TOGGLE_ACTIVE[] = L"powertoys.keyboardManager.toggleActive";
const wchar_t ACTION_ID_OPEN_EDITOR[] = L"powertoys.keyboardManager.openEditor";
const wchar_t ACTION_ID_MAPPING_PREFIX[] = L"powertoys.keyboardManager.mapping.";
const wchar_t ACTION_CATEGORY[] = L"Keyboard Manager";
struct ActionInvokeResult
{
bool success = true;
std::wstring error_code;
std::wstring message;
};
struct handle_data
{
unsigned long process_id;
HWND window_handle;
};
ActionInvokeResult action_error(std::wstring error_code, std::wstring message)
{
return ActionInvokeResult{ .success = false, .error_code = std::move(error_code), .message = std::move(message) };
}
bool is_keyboard_manager_custom_action(const KeyShortcutTextUnion& target)
{
if (target.index() != 1)
{
return false;
}
const auto& shortcut = std::get<Shortcut>(target);
return shortcut.IsRunProgram() || shortcut.IsOpenURI();
}
BOOL CALLBACK enum_windows_callback_allow_non_visible(HWND handle, LPARAM l_param)
{
handle_data& data = *reinterpret_cast<handle_data*>(l_param);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id == process_id)
{
data.window_handle = handle;
return FALSE;
}
return TRUE;
}
BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM l_param)
{
handle_data& data = *reinterpret_cast<handle_data*>(l_param);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id != process_id || !(GetWindow(handle, GW_OWNER) == static_cast<HWND>(0) && IsWindowVisible(handle)))
{
return TRUE;
}
data.window_handle = handle;
return FALSE;
}
HWND find_main_window(unsigned long process_id, bool allow_non_visible)
{
handle_data data{ .process_id = process_id, .window_handle = nullptr };
EnumWindows(allow_non_visible ? enum_windows_callback_allow_non_visible : enum_windows_callback, reinterpret_cast<LPARAM>(&data));
return data.window_handle;
}
std::vector<DWORD> get_processes_id_by_name(const std::wstring& process_name)
{
std::vector<DWORD> process_ids;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
{
return process_ids;
}
PROCESSENTRY32 process_entry{};
process_entry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &process_entry))
{
do
{
if (_wcsicmp(process_entry.szExeFile, process_name.c_str()) == 0)
{
process_ids.push_back(process_entry.th32ProcessID);
}
} while (Process32Next(snapshot, &process_entry));
}
CloseHandle(snapshot);
return process_ids;
}
DWORD get_process_id_by_name(const std::wstring& process_name)
{
const auto process_ids = get_processes_id_by_name(process_name);
return process_ids.empty() ? 0 : process_ids.front();
}
std::wstring get_file_name_from_path(const std::wstring& full_path)
{
const size_t found = full_path.find_last_of(L"\\/");
return found == std::wstring::npos ? full_path : full_path.substr(found + 1);
}
void close_process_by_name(const std::wstring& file_name_part)
{
auto process_ids = get_processes_id_by_name(file_name_part);
if (process_ids.empty())
{
return;
}
std::thread([file_name_part]() {
auto retry_count = 10;
auto local_process_ids = get_processes_id_by_name(file_name_part);
while (!local_process_ids.empty() && retry_count-- > 0)
{
for (DWORD pid : local_process_ids)
{
HWND hwnd = find_main_window(pid, false);
if (hwnd)
{
SendMessage(hwnd, WM_CLOSE, 0, 0);
}
Sleep(10);
}
local_process_ids = get_processes_id_by_name(file_name_part);
if (!local_process_ids.empty())
{
Sleep(100);
}
}
}).detach();
}
void terminate_processes_by_name(const std::wstring& file_name_part)
{
for (DWORD pid : get_processes_id_by_name(file_name_part))
{
HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (process)
{
TerminateProcess(process, 0);
CloseHandle(process);
}
}
}
bool hide_program(DWORD pid, const std::wstring& program_name, int retry_count)
{
Logger::trace(L"KeyboardManager action: hide {} pid={} retry={}", program_name, pid, retry_count);
HWND hwnd = find_main_window(pid, false);
if (hwnd == nullptr && retry_count < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return hide_program(pid, program_name, retry_count + 1);
});
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pid_for_hwnd = 0;
GetWindowThreadProcessId(hwnd, &pid_for_hwnd);
if (pid == pid_for_hwnd && IsWindowVisible(hwnd))
{
ShowWindow(hwnd, SW_HIDE);
}
hwnd = FindWindowEx(nullptr, hwnd, nullptr, nullptr);
}
return true;
}
bool show_program(DWORD pid, const std::wstring& program_name, bool is_new_process, bool minimize_if_visible, int retry_count)
{
Logger::trace(L"KeyboardManager action: show {} pid={} retry={}", program_name, pid, retry_count);
HWND hwnd = find_main_window(pid, false);
if (hwnd == nullptr)
{
if (retry_count < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return show_program(pid, program_name, is_new_process, minimize_if_visible, retry_count + 1);
});
}
}
else
{
if (hwnd == GetForegroundWindow())
{
if (!is_new_process && minimize_if_visible)
{
return ShowWindow(hwnd, SW_MINIMIZE);
}
return false;
}
if (IsIconic(hwnd) && !ShowWindow(hwnd, SW_RESTORE))
{
Logger::warn(L"KeyboardManager action: failed restoring {}", program_name);
}
INPUT inputs[1] = { { .type = INPUT_MOUSE } };
SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
return SetForegroundWindow(hwnd);
}
if (is_new_process)
{
return true;
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pid_for_hwnd = 0;
GetWindowThreadProcessId(hwnd, &pid_for_hwnd);
if (pid == pid_for_hwnd)
{
const int length = GetWindowTextLength(hwnd);
if (length > 0)
{
ShowWindow(hwnd, SW_RESTORE);
if (SetForegroundWindow(hwnd))
{
return true;
}
}
}
hwnd = FindWindowEx(nullptr, hwnd, nullptr, nullptr);
}
return false;
}
std::wstring expand_environment_string(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const DWORD size = ExpandEnvironmentStrings(value.c_str(), nullptr, 0);
if (size == 0)
{
return value;
}
std::wstring expanded(size, L'\0');
if (ExpandEnvironmentStrings(value.c_str(), expanded.data(), size) == 0)
{
return value;
}
if (!expanded.empty() && expanded.back() == L'\0')
{
expanded.pop_back();
}
return expanded;
}
std::wstring build_command_line(const std::wstring& file, const std::wstring& params)
{
std::wstring command_line = L"\"" + file + L"\"";
if (!params.empty())
{
command_line += L" " + params;
}
return command_line;
}
bool launch_process_same_user(const std::wstring& file, const std::wstring& params, const wchar_t* working_directory, bool show_window, DWORD& process_id)
{
auto command_line = build_command_line(file, params);
STARTUPINFOW startup_info{};
startup_info.cb = sizeof(startup_info);
DWORD creation_flags = 0;
if (!show_window)
{
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
creation_flags = CREATE_NO_WINDOW;
}
PROCESS_INFORMATION process_info{};
const auto launched = CreateProcessW(
file.c_str(),
command_line.data(),
nullptr,
nullptr,
FALSE,
creation_flags,
nullptr,
working_directory,
&startup_info,
&process_info);
if (!launched)
{
Logger::error(L"KeyboardManager action: CreateProcessW failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
process_id = process_info.hProcess ? GetProcessId(process_info.hProcess) : 0;
if (process_info.hThread)
{
CloseHandle(process_info.hThread);
}
if (process_info.hProcess)
{
CloseHandle(process_info.hProcess);
}
return process_id != 0;
}
bool launch_process_shell_execute(const wchar_t* verb, const std::wstring& file, const std::wstring& params, const wchar_t* working_directory, bool show_window, DWORD& process_id)
{
SHELLEXECUTEINFOW execute_info{};
execute_info.cbSize = sizeof(execute_info);
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
execute_info.lpVerb = verb;
execute_info.lpFile = file.c_str();
execute_info.lpParameters = params.empty() ? nullptr : params.c_str();
execute_info.lpDirectory = working_directory;
execute_info.nShow = show_window ? SW_SHOWDEFAULT : SW_HIDE;
if (!ShellExecuteExW(&execute_info))
{
Logger::error(L"KeyboardManager action: ShellExecuteExW failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
process_id = execute_info.hProcess ? GetProcessId(execute_info.hProcess) : 0;
if (execute_info.hProcess)
{
CloseHandle(execute_info.hProcess);
}
return process_id != 0;
}
ActionInvokeResult invoke_run_program_action(const Shortcut& shortcut)
{
const std::wstring full_expanded_file_path = expand_environment_string(shortcut.runProgramFilePath);
const std::wstring file_name_part = get_file_name_from_path(full_expanded_file_path);
const DWORD target_pid = get_process_id_by_name(file_name_part);
if (target_pid != 0 && shortcut.alreadyRunningAction != Shortcut::ProgramAlreadyRunningAction::StartAnother)
{
if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::EndTask)
{
terminate_processes_by_name(file_name_part);
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::Close)
{
close_process_by_name(file_name_part);
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::ShowWindow)
{
const auto process_ids = get_processes_id_by_name(file_name_part);
for (DWORD pid : process_ids)
{
show_program(pid, file_name_part, false, false, 0);
}
}
return {};
}
if (GetFileAttributesW(full_expanded_file_path.c_str()) == INVALID_FILE_ATTRIBUTES)
{
return action_error(L"program_not_found", L"The program '" + file_name_part + L"' was not found.");
}
const std::wstring expanded_args = expand_environment_string(shortcut.runProgramArgs);
const std::wstring expanded_start_dir = expand_environment_string(shortcut.runProgramStartInDir);
const wchar_t* current_dir_ptr = expanded_start_dir.empty() ? nullptr : expanded_start_dir.c_str();
if (current_dir_ptr && GetFileAttributesW(current_dir_ptr) == INVALID_FILE_ATTRIBUTES)
{
return action_error(L"invalid_start_directory", L"The configured start-in path is invalid.");
}
DWORD process_id = 0;
const bool show_window = shortcut.startWindowType == Shortcut::StartWindowType::Normal;
bool launched = false;
if (shortcut.elevationLevel == Shortcut::ElevationLevel::Elevated)
{
launched = launch_process_shell_execute(L"runas", full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
else if (shortcut.elevationLevel == Shortcut::ElevationLevel::DifferentUser)
{
launched = launch_process_shell_execute(L"runAsUser", full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
else
{
launched = launch_process_same_user(full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
if (!launched || process_id == 0)
{
return action_error(L"launch_failed", L"The application might not have started.");
}
if (shortcut.startWindowType == Shortcut::StartWindowType::Hidden)
{
hide_program(process_id, file_name_part, 0);
}
return {};
}
ActionInvokeResult invoke_open_uri_action(const Shortcut& shortcut)
{
std::wstring target = shortcut.uriToOpen;
if (target.empty())
{
return action_error(L"invalid_uri", L"The configured path or URI is empty.");
}
if (!PathIsURLW(target.c_str()))
{
wchar_t url[2048]{};
DWORD buffer_size = ARRAYSIZE(url);
if (UrlCreateFromPathW(target.c_str(), url, &buffer_size, 0) != S_OK)
{
return action_error(L"invalid_uri", L"Could not understand the configured path or URI.");
}
target = url;
}
const auto result = reinterpret_cast<INT_PTR>(ShellExecuteW(nullptr, L"open", target.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result <= 32)
{
return action_error(L"invoke_failed", L"Could not open the configured path or URI.");
}
return {};
}
ActionInvokeResult invoke_keyboard_manager_custom_action(const Shortcut& shortcut)
{
if (shortcut.IsRunProgram())
{
return invoke_run_program_action(shortcut);
}
if (shortcut.IsOpenURI())
{
return invoke_open_uri_action(shortcut);
}
return action_error(L"unsupported_action", L"Keyboard Manager only exposes Run Program and Open URL mappings as invokable actions.");
}
uint64_t fnv1a_hash(const std::wstring& value)
{
uint64_t hash = 1469598103934665603ull;
for (wchar_t ch : value)
{
hash ^= static_cast<uint64_t>(ch);
hash *= 1099511628211ull;
}
return hash;
}
std::wstring mapping_action_id(const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
std::wstringstream identity;
identity << source.ToHstringVK().c_str() << L'|' << source.exactMatch << L'|' << app << L'|' << static_cast<int>(target.operationType) << L'|';
if (target.IsRunProgram())
{
identity << target.runProgramFilePath << L'|' << target.runProgramArgs << L'|' << target.runProgramStartInDir << L'|' << static_cast<int>(target.elevationLevel) << L'|' << static_cast<int>(target.alreadyRunningAction) << L'|' << static_cast<int>(target.startWindowType);
}
else if (target.IsOpenURI())
{
identity << target.uriToOpen;
}
std::wstringstream action_id;
action_id << ACTION_ID_MAPPING_PREFIX << std::hex << std::setfill(L'0') << std::setw(16) << fnv1a_hash(identity.str());
return action_id.str();
}
std::wstring mapping_action_display_name(const Shortcut& target)
{
if (target.IsRunProgram())
{
const auto expanded_path = expand_environment_string(target.runProgramFilePath);
const auto file_name = get_file_name_from_path(expanded_path.empty() ? target.runProgramFilePath : expanded_path);
return file_name.empty() ? L"Run Keyboard Manager program action" : (L"Run " + file_name);
}
if (target.IsOpenURI())
{
return L"Open " + target.uriToOpen;
}
return L"Keyboard Manager action";
}
std::wstring mapping_action_description(const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
const std::wstring scope = app.empty() ? L"Global" : (L"App-specific (" + app + L")");
const std::wstring target_description = target.IsRunProgram() ? target.runProgramFilePath : target.uriToOpen;
return scope + L" Keyboard Manager shortcut " + source.ToHstringVK().c_str() + L" -> " + target_description;
}
void append_mapping_action(json::JsonArray& actions, const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
json::JsonObject action;
action.SetNamedValue(L"action_id", json::value(mapping_action_id(source, target, app)));
action.SetNamedValue(L"display_name", json::value(mapping_action_display_name(target)));
action.SetNamedValue(L"description", json::value(mapping_action_description(source, target, app)));
action.SetNamedValue(L"category", json::value(ACTION_CATEGORY));
action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(action);
}
void append_mapping_actions(json::JsonArray& actions, const MappingConfiguration& config)
{
for (const auto& [source, remap] : config.osLevelShortcutReMap)
{
if (is_keyboard_manager_custom_action(remap.targetShortcut))
{
append_mapping_action(actions, source, std::get<Shortcut>(remap.targetShortcut), L"");
}
}
for (const auto& [app, remaps] : config.appSpecificShortcutReMap)
{
for (const auto& [source, remap] : remaps)
{
if (is_keyboard_manager_custom_action(remap.targetShortcut))
{
append_mapping_action(actions, source, std::get<Shortcut>(remap.targetShortcut), app);
}
}
}
}
std::optional<Shortcut> find_mapping_action(const std::wstring& action_id)
{
MappingConfiguration config;
if (!config.LoadSettings())
{
return std::nullopt;
}
for (const auto& [source, remap] : config.osLevelShortcutReMap)
{
if (!is_keyboard_manager_custom_action(remap.targetShortcut))
{
continue;
}
const auto& target = std::get<Shortcut>(remap.targetShortcut);
if (mapping_action_id(source, target, L"") == action_id)
{
return target;
}
}
for (const auto& [app, remaps] : config.appSpecificShortcutReMap)
{
for (const auto& [source, remap] : remaps)
{
if (!is_keyboard_manager_custom_action(remap.targetShortcut))
{
continue;
}
const auto& target = std::get<Shortcut>(remap.targetShortcut);
if (mapping_action_id(source, target, app) == action_id)
{
return target;
}
}
}
return std::nullopt;
}
bool write_marshaled_json(const std::wstring& json, wchar_t* buffer, int* buffer_size)
{
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(json.size() + 1);
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, json.c_str());
return true;
}
}
// Implement the PowerToy Module Interface and all the required methods.
class KeyboardManager : public PowertoyModuleIface
@@ -355,13 +983,91 @@ public:
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
// Signal from the Settings editor to call a custom action.
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
virtual bool get_actions(wchar_t* buffer, int* buffer_size) override
{
json::JsonArray actions;
json::JsonObject toggle_action;
toggle_action.SetNamedValue(L"action_id", json::value(ACTION_ID_TOGGLE_ACTIVE));
toggle_action.SetNamedValue(L"display_name", json::value(L"Toggle Keyboard Manager active state"));
toggle_action.SetNamedValue(L"description", json::value(L"Turns the Keyboard Manager engine on or off."));
toggle_action.SetNamedValue(L"category", json::value(L"Keyboard Manager"));
toggle_action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(toggle_action);
json::JsonObject open_editor_action;
open_editor_action.SetNamedValue(L"action_id", json::value(ACTION_ID_OPEN_EDITOR));
open_editor_action.SetNamedValue(L"display_name", json::value(L"Open Keyboard Manager editor"));
open_editor_action.SetNamedValue(L"description", json::value(L"Opens the new Keyboard Manager editor window."));
open_editor_action.SetNamedValue(L"category", json::value(L"Keyboard Manager"));
open_editor_action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(open_editor_action);
MappingConfiguration config;
if (config.LoadSettings())
{
append_mapping_actions(actions, config);
}
return write_marshaled_json(actions.Stringify().c_str(), buffer, buffer_size);
}
virtual bool invoke_action(const wchar_t* action_id, const wchar_t* /*serialized_args*/, wchar_t* buffer, int* buffer_size) override
{
json::JsonObject result;
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
std::wstring requested_action = action_id ? action_id : L"";
if (requested_action == ACTION_ID_TOGGLE_ACTIVE)
{
toggle_engine();
}
else if (requested_action == ACTION_ID_OPEN_EDITOR)
{
if (!launch_editor())
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"launch_failed"));
result.SetNamedValue(L"message", json::value(L"Failed to open the Keyboard Manager editor."));
}
}
else if (requested_action.rfind(ACTION_ID_MAPPING_PREFIX, 0) == 0)
{
const auto mapped_action = find_mapping_action(requested_action);
if (!mapped_action.has_value())
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"action_not_found"));
result.SetNamedValue(L"message", json::value(L"The requested Keyboard Manager shortcut action no longer exists."));
}
else
{
const auto action_result = invoke_keyboard_manager_custom_action(*mapped_action);
if (!action_result.success)
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(action_result.error_code));
result.SetNamedValue(L"message", json::value(action_result.message));
}
}
}
else
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"unsupported_action"));
result.SetNamedValue(L"message", json::value(L"Keyboard Manager does not recognize the requested action."));
}
return write_marshaled_json(result.Stringify().c_str(), buffer, buffer_size);
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{

View File

@@ -0,0 +1,227 @@
#include "pch.h"
#include "action_pipe_server.h"
#include "action_registry.h"
#include <common/interop/shared_constants.h>
#include <common/logger/logger.h>
#include <vector>
namespace
{
constexpr DWORD MESSAGE_LENGTH_BYTES = sizeof(std::uint32_t);
constexpr DWORD BUFFER_BYTES = 64 * 1024;
std::wstring Utf8ToUtf16(const std::string& value)
{
if (value.empty())
{
return {};
}
const int size = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (size <= 0)
{
return {};
}
std::wstring result(static_cast<size_t>(size), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size);
return result;
}
std::string Utf16ToUtf8(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const int size = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (size <= 0)
{
return {};
}
std::string result(static_cast<size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size, nullptr, nullptr);
return result;
}
bool ReadExact(HANDLE handle, void* buffer, DWORD bytes_to_read)
{
auto* target = static_cast<std::byte*>(buffer);
DWORD total_read = 0;
while (total_read < bytes_to_read)
{
DWORD bytes_read = 0;
if (!ReadFile(handle, target + total_read, bytes_to_read - total_read, &bytes_read, nullptr))
{
return false;
}
if (bytes_read == 0)
{
return false;
}
total_read += bytes_read;
}
return true;
}
bool WriteExact(HANDLE handle, const void* buffer, DWORD bytes_to_write)
{
const auto* source = static_cast<const std::byte*>(buffer);
DWORD total_written = 0;
while (total_written < bytes_to_write)
{
DWORD bytes_written = 0;
if (!WriteFile(handle, source + total_written, bytes_to_write - total_written, &bytes_written, nullptr))
{
return false;
}
total_written += bytes_written;
}
return true;
}
json::JsonObject ErrorResponse(const std::wstring& error_code, const std::wstring& message)
{
json::JsonObject response;
response.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
response.SetNamedValue(L"error_code", json::value(error_code));
response.SetNamedValue(L"message", json::value(message));
return response;
}
json::JsonObject HandleRequestPayload(const std::string& payload)
{
json::JsonObject request;
if (!json::JsonObject::TryParse(Utf8ToUtf16(payload), request))
{
return ErrorResponse(L"invalid_request", L"The action pipe request payload was not valid JSON.");
}
const std::wstring request_type = request.GetNamedString(L"type", L"").c_str();
if (request_type == L"list_actions")
{
json::JsonObject response;
response.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
response.SetNamedValue(L"actions", PowerToysActionRegistry::Instance().ListActions());
return response;
}
if (request_type == L"invoke_action")
{
const std::wstring action_id = request.GetNamedString(L"action_id", L"").c_str();
const std::wstring arguments = request.GetNamedString(L"arguments", L"{}").c_str();
return PowerToysActionRegistry::Instance().InvokeAction(action_id, arguments);
}
return ErrorResponse(L"unsupported_request", L"The action pipe request type is not supported.");
}
void WakeActionPipeServer()
{
HANDLE client = CreateFileW(
CommonSharedConstants::POWERTOYS_ACTIONS_PIPE,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (client != INVALID_HANDLE_VALUE)
{
CloseHandle(client);
}
}
}
RunnerActionPipeServer::~RunnerActionPipeServer()
{
Stop();
}
void RunnerActionPipeServer::Start()
{
if (_server_thread.joinable())
{
return;
}
_stop_requested = false;
_server_thread = std::thread(&RunnerActionPipeServer::Run, this);
}
void RunnerActionPipeServer::Stop()
{
_stop_requested = true;
WakeActionPipeServer();
if (_server_thread.joinable())
{
_server_thread.join();
}
}
void RunnerActionPipeServer::Run()
{
while (!_stop_requested)
{
HANDLE pipe = CreateNamedPipeW(
CommonSharedConstants::POWERTOYS_ACTIONS_PIPE,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFFER_BYTES,
BUFFER_BYTES,
0,
nullptr);
if (pipe == INVALID_HANDLE_VALUE)
{
Logger::error(L"RunnerActionPipeServer: failed to create named pipe. error={}", GetLastError());
return;
}
const BOOL connected = ConnectNamedPipe(pipe, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (!connected)
{
Logger::warn(L"RunnerActionPipeServer: client failed to connect. error={}", GetLastError());
CloseHandle(pipe);
continue;
}
std::uint32_t request_length = 0;
if (ReadExact(pipe, &request_length, MESSAGE_LENGTH_BYTES))
{
std::vector<char> request_payload(request_length);
const bool payload_ok = request_length == 0 || ReadExact(pipe, request_payload.data(), request_length);
json::JsonObject response = payload_ok
? HandleRequestPayload(std::string(request_payload.begin(), request_payload.end()))
: ErrorResponse(L"read_failed", L"Failed to read the action pipe request payload.");
const auto response_utf8 = Utf16ToUtf8(response.Stringify().c_str());
const auto response_length = static_cast<std::uint32_t>(response_utf8.size());
WriteExact(pipe, &response_length, MESSAGE_LENGTH_BYTES);
if (response_length > 0)
{
WriteExact(pipe, response_utf8.data(), response_length);
}
}
FlushFileBuffers(pipe);
DisconnectNamedPipe(pipe);
CloseHandle(pipe);
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <atomic>
#include <thread>
class RunnerActionPipeServer
{
public:
RunnerActionPipeServer() = default;
~RunnerActionPipeServer();
void Start();
void Stop();
private:
std::atomic<bool> _stop_requested{ false };
std::thread _server_thread;
void Run();
};

View File

@@ -0,0 +1,162 @@
#include "pch.h"
#include "action_registry.h"
#include "powertoy_module.h"
#include <common/logger/logger.h>
namespace
{
constexpr wchar_t ACTION_ID_PROPERTY[] = L"action_id";
constexpr wchar_t MODULE_KEY_PROPERTY[] = L"module_key";
constexpr wchar_t AVAILABLE_PROPERTY[] = L"available";
}
PowerToysActionRegistry& PowerToysActionRegistry::Instance()
{
static PowerToysActionRegistry instance;
return instance;
}
json::JsonObject PowerToysActionRegistry::ErrorResult(const std::wstring& error_code, const std::wstring& message)
{
json::JsonObject result;
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(error_code));
result.SetNamedValue(L"message", json::value(message));
return result;
}
void PowerToysActionRegistry::RefreshLocked()
{
actions.clear();
duplicate_action_ids.clear();
for (const auto& [module_key, module] : modules())
{
json::JsonArray descriptors;
try
{
descriptors = module.json_actions();
}
catch (...)
{
Logger::error(L"PowerToysActionRegistry: malformed actions from module {}", module_key);
continue;
}
for (const auto& value : descriptors)
{
if (value.ValueType() != json::JsonValueType::Object)
{
Logger::warn(L"PowerToysActionRegistry: ignoring non-object action descriptor from module {}", module_key);
continue;
}
auto descriptor = value.GetObjectW();
const auto action_id_hstring = descriptor.GetNamedString(ACTION_ID_PROPERTY, L"");
const std::wstring action_id = action_id_hstring.c_str();
if (action_id.empty())
{
Logger::warn(L"PowerToysActionRegistry: ignoring action without action_id from module {}", module_key);
continue;
}
if (actions.contains(action_id))
{
actions.erase(action_id);
duplicate_action_ids.insert(action_id);
Logger::error(L"PowerToysActionRegistry: duplicate action_id {} detected", action_id);
continue;
}
if (duplicate_action_ids.contains(action_id))
{
continue;
}
actions.emplace(action_id, RegisteredAction{
.module_key = module_key,
.descriptor = descriptor,
});
}
}
}
json::JsonArray PowerToysActionRegistry::ListActions()
{
std::scoped_lock lock{ mutex };
RefreshLocked();
json::JsonArray result;
for (const auto& [action_id, registered_action] : actions)
{
auto descriptor = registered_action.descriptor;
descriptor.SetNamedValue(ACTION_ID_PROPERTY, json::value(action_id));
descriptor.SetNamedValue(MODULE_KEY_PROPERTY, json::value(registered_action.module_key));
const auto module_it = modules().find(registered_action.module_key);
const bool is_available = module_it != modules().end() && module_it->second->is_enabled();
descriptor.SetNamedValue(AVAILABLE_PROPERTY, json::JsonValue::CreateBooleanValue(is_available));
result.Append(descriptor);
}
return result;
}
json::JsonObject PowerToysActionRegistry::InvokeAction(const std::wstring& action_id, const std::wstring& serialized_args)
{
std::scoped_lock lock{ mutex };
RefreshLocked();
if (duplicate_action_ids.contains(action_id))
{
return ErrorResult(L"duplicate_action_id", L"Multiple modules registered the same action identifier.");
}
const auto action_it = actions.find(action_id);
if (action_it == actions.end())
{
return ErrorResult(L"action_not_found", L"The requested PowerToys action is not registered.");
}
const auto module_it = modules().find(action_it->second.module_key);
if (module_it == modules().end())
{
return ErrorResult(L"module_not_found", L"The module that owns this action is not loaded.");
}
if (!module_it->second->is_enabled())
{
return ErrorResult(L"module_unavailable", L"The module that owns this action is currently disabled.");
}
std::wstring raw_result;
try
{
raw_result = module_it->second.invoke_action(action_id, serialized_args);
}
catch (...)
{
return ErrorResult(L"invoke_failed", L"The module threw while invoking the requested action.");
}
if (raw_result.empty())
{
return ErrorResult(L"empty_result", L"The module returned an empty result.");
}
json::JsonObject result;
if (!json::JsonObject::TryParse(raw_result, result))
{
return ErrorResult(L"invalid_result", L"The module returned malformed action result JSON.");
}
if (!result.HasKey(L"success"))
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
}
return result;
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <common/utils/json.h>
#include <map>
#include <mutex>
#include <set>
#include <string>
class PowerToysActionRegistry
{
public:
static PowerToysActionRegistry& Instance();
json::JsonArray ListActions();
json::JsonObject InvokeAction(const std::wstring& action_id, const std::wstring& serialized_args);
private:
struct RegisteredAction
{
std::wstring module_key;
json::JsonObject descriptor;
};
std::mutex mutex;
std::map<std::wstring, RegisteredAction> actions;
std::set<std::wstring> duplicate_action_ids;
static json::JsonObject ErrorResult(const std::wstring& error_code, const std::wstring& message);
void RefreshLocked();
};

View File

@@ -27,8 +27,9 @@
#include <common/utils/resources.h>
#include <common/utils/clean_video_conference.h>
#include "UpdateUtils.h"
#include "ActionRunnerUtils.h"
#include "UpdateUtils.h"
#include "ActionRunnerUtils.h"
#include "action_pipe_server.h"
#include <winrt/Windows.System.h>
@@ -289,8 +290,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
// L"PowerToys.PowerDisplayModuleInterface.dll", // TEMPORARILY_DISABLED: PowerDisplay
};
for (auto moduleSubdir : knownModules)
{
for (auto moduleSubdir : knownModules)
{
try
{
auto pt_module = load_powertoy(moduleSubdir);
@@ -314,9 +315,13 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
MB_OK | MB_ICONERROR);
#endif
}
}
// Start initial powertoys
start_enabled_powertoys();
}
RunnerActionPipeServer action_pipe_server;
action_pipe_server.Start();
// Start initial powertoys
start_enabled_powertoys();
std::wstring product_version = get_product_version();
Trace::EventLaunch(product_version, isProcessElevated);
PTSettingsHelper::save_last_version_run(product_version);
@@ -341,9 +346,10 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
open_scoobe_window();
}
settings_telemetry::init();
result = run_message_loop();
}
settings_telemetry::init();
result = run_message_loop();
action_pipe_server.Stop();
}
catch (std::runtime_error& err)
{
std::string err_what = err.what();

View File

@@ -29,15 +29,59 @@ PowertoyModule load_powertoy(const std::wstring_view filename)
return PowertoyModule(pt_module, handle);
}
json::JsonObject PowertoyModule::json_config() const
{
int size = 0;
pt_module->get_config(nullptr, &size);
json::JsonObject PowertoyModule::json_config() const
{
int size = 0;
pt_module->get_config(nullptr, &size);
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
pt_module->get_config(result.data(), &size);
return json::JsonObject::Parse(result);
}
pt_module->get_config(result.data(), &size);
return json::JsonObject::Parse(result);
}
json::JsonArray PowertoyModule::json_actions() const
{
int size = 0;
pt_module->get_actions(nullptr, &size);
if (size <= 0)
{
return json::JsonArray{};
}
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
if (!pt_module->get_actions(result.data(), &size))
{
return json::JsonArray{};
}
json::JsonArray actions;
if (!json::JsonArray::TryParse(result, actions))
{
return json::JsonArray{};
}
return actions;
}
std::wstring PowertoyModule::invoke_action(const std::wstring& action_id, const std::wstring& serialized_args) const
{
int size = 0;
pt_module->invoke_action(action_id.c_str(), serialized_args.c_str(), nullptr, &size);
if (size <= 0)
{
return {};
}
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
if (!pt_module->invoke_action(action_id.c_str(), serialized_args.c_str(), result.data(), &size))
{
return {};
}
return result;
}
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) :
handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())

View File

@@ -39,9 +39,13 @@ public:
return pt_module.get();
}
json::JsonObject json_config() const;
void update_hotkeys();
json::JsonObject json_config() const;
json::JsonArray json_actions() const;
std::wstring invoke_action(const std::wstring& action_id, const std::wstring& serialized_args) const;
void update_hotkeys();
void UpdateHotkeyEx();

View File

@@ -59,8 +59,10 @@
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\common\interop\two_way_pipe_message_ipc.cpp" />
<ClCompile Include="auto_start_helper.cpp" />
<ClCompile Include="..\common\interop\two_way_pipe_message_ipc.cpp" />
<ClCompile Include="action_pipe_server.cpp" />
<ClCompile Include="action_registry.cpp" />
<ClCompile Include="auto_start_helper.cpp" />
<ClCompile Include="bug_report.cpp" />
<ClCompile Include="centralized_hotkeys.cpp" />
<ClCompile Include="general_settings.cpp" />
@@ -80,8 +82,10 @@
<ClCompile Include="unhandled_exception_handler.cpp" />
<ClCompile Include="UpdateUtils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="ActionRunnerUtils.h" />
<ItemGroup>
<ClInclude Include="action_pipe_server.h" />
<ClInclude Include="action_registry.h" />
<ClInclude Include="ActionRunnerUtils.h" />
<ClInclude Include="ai_detection.h" />
<ClInclude Include="auto_start_helper.h" />
<ClInclude Include="bug_report.h" />
@@ -176,4 +180,4 @@
</ClCompile>
</ItemGroup>
</Target>
</Project>
</Project>