Skip to content

[Keyboard Manager] Add "alone" (tap) condition for single-key remaps (dual-key / to_if_alone)#49136

Open
MasaYan24 wants to merge 7 commits into
microsoft:mainfrom
MasaYan24:feature/kbm-dual-key-alone
Open

[Keyboard Manager] Add "alone" (tap) condition for single-key remaps (dual-key / to_if_alone)#49136
MasaYan24 wants to merge 7 commits into
microsoft:mainfrom
MasaYan24:feature/kbm-dual-key-alone

Conversation

@MasaYan24

@MasaYan24 MasaYan24 commented Jul 5, 2026

Copy link
Copy Markdown

Summary of the Pull Request

Adds an optional "alone" condition to single-key remaps in Keyboard Manager, bringing
Karabiner-style to_if_alone (dual-key / tap-vs-hold) behavior: a key can perform one action
when tapped by itself while still acting as its normal self when combined with other keys.

Example use case: Right Ctrl → IME On when tapped alone, Left Ctrl → IME Off when tapped alone,
while Right/Left Ctrl + other keys keep working as normal Ctrl-based shortcuts. This is the
Windows equivalent of the popular macOS "left/right ⌘ → 英数/かな" mapping.

PR Checklist

Detailed Description of the Pull Request / Additional comments

Engine (C++):

  • New separate remap table aloneSingleKeyReMap (parallel to singleKeyReMap) with
    Add/Clear/GetSingleKeyAloneRemap (MappingConfiguration).
  • New handler HandleSingleKeyAloneRemapEvent using a lazy + release-fire strategy:
    suppress the alone key's key-down; on the first other key it flushes the original key-down
    (promoting to a normal combination); on solo release it fires the alone action. Wired into the
    hook dispatch before the normal single-key handler; per-key runtime state lives in State.
  • Settings JSON: an optional "condition" field per single-key remap ("always" default, or
    "alone"). Fully backward compatible; both load and save handle it, so hand-edited or
    UI-created alone remaps round-trip without loss.

Editor UI (WinUI3):

  • Native wrapper exposes parallel alone add/get/delete over the alone table (reuses the existing
    SingleKeyMapping marshaling struct).
  • SingleKeyRemapCondition (Always / Alone) flows through the C# interop/service/model layers.
  • A localized "Condition" combo box in the add/edit dialog, shown only for a single-key
    remap whose action is a key/shortcut
    (the case the engine supports), plus a localized
    "Alone" badge on such rows in the list.

Known limitations / follow-ups (intentionally not in this PR):

  • Hold-timeout not implemented (holding the alone key then releasing still fires the alone action;
    needs an injectable clock — deferred, noted in code).
  • Alone target supports a single key or a shortcut (not text/URL/app).

Validation Steps Performed

  • Engine unit tests: 108/108 (incl. 5 new alone tests + edge cases). Editor C++ tests: 88/88.
  • Editor UI project compiles clean (0 warnings / 0 errors).
  • Manual E2E on real hardware (self-built x64/Debug):
    • Right Ctrl tapped alone → IME On; Left Ctrl tapped alone → IME Off (directional, idempotent).
    • Ctrl+C / V / Z / A and existing Ctrl-based custom shortcuts unaffected (modifier pass-through OK).
    • Editor UI: opening an existing alone remap shows Condition = "Alone (tap)"; saving round-trips a
      full real-world config (single-key + 70 global + 16 app-specific shortcuts) with zero loss;
      the "Alone" badge and the show/hide gating of the Condition combo both behave correctly.

MasaYan24 and others added 7 commits July 5, 2026 10:10
…y tap/hold)

Add a dual-key remap capability to the KBM engine: a single key can carry an
"Alone" action that applies only when the key is tapped alone, while using it in
combination with other keys passes the original key through as a real key/modifier
(equivalent to Karabiner's to_if_alone). Uses the lazy + release-fire approach so
the combination path has zero added latency.

- New aloneSingleKeyReMap table plus Add/Clear/GetSingleKeyAloneRemap on
  MappingConfiguration/State, kept separate from the existing singleKeyReMap.
- HandleSingleKeyAloneRemapEvent: suppresses the physical key-down (lazy); on
  another key-down it injects the original key as a real modifier (combination);
  on release with no intervening key it injects the alone action as a tap.
- Per-key runtime state (pending vs combination) tracked in State across the
  separate key-down/key-up hook invocations.
- 3 unit tests: no-injection-on-keydown, tap-alone injection, combination
  pass-through.

Timeout-based cancellation (holding too long is not a tap) is intentionally
deferred; it needs an injectable clock to be unit-testable. The handler is not
yet wired into the runtime dispatch chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ition" from settings

- Call HandleSingleKeyAloneRemapEvent before HandleSingleKeyRemapEvent in the
  keyboard hook dispatch so alone-mapped keys are handled with priority (lazy
  key-down hold, tap-vs-combination decision).
- Include aloneSingleKeyReMap in HasRegisteredRemappingsUnchecked so a config
  with only alone remaps still activates the engine.
- Load an optional "condition" field on single-key remaps: "alone" routes the
  entry to the alone table; a missing field (or "always") keeps the legacy
  unconditional behavior, so existing settings files load unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…andler

- Add unit tests for a disabled alone target (tapping injects nothing) and for a
  solo tap after a prior combination cycle (per-key runtime state resets cleanly).
- Document HandleSingleKeyAloneRemapEvent and the optional "condition" settings
  field in the keyboard event handlers devdoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SaveSettingsToFile previously wrote only singleKeyReMap and never persisted the
aloneSingleKeyReMap table, so an alone remap loaded from settings was silently
dropped the next time settings were saved. Add a serialization loop over
aloneSingleKeyReMap that appends each entry to the inProcess array tagged with
condition="alone", round-tripping with the (already condition-aware) loader.
Regular remaps still omit the field and load as "always", so existing settings
files are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dition

Wire the "Alone" single-key remap condition through the WinUI3 editor so it can be
created, edited, saved and reloaded. Previously the engine understood the condition
and round-tripped it in settings, but the editor had no way to set or show it.

- Native wrapper: parallel GetSingleKeyAloneRemapCount / GetSingleKeyAloneRemap /
  AddSingleKeyAloneRemap / AddSingleKeyAloneToShortcutRemap / DeleteSingleKeyAloneRemap
  over the aloneSingleKeyReMap table (reuses the existing SingleKeyMapping struct, so
  no marshaling changes).
- C# interop/service: expose the alone add/get/delete and tag read-back mappings with
  IsAlone.
- Model: SingleKeyRemapCondition (Always/Alone) on Remapping and ShortcutKeyMapping
  (including Equals/GetHashCode); settings correlation carries it.
- UI: a Condition combo box on the mapping dialog; save/load/delete route to the alone
  table when the condition is Alone.

Localizing the combo box (x:Uid/resw) and gating its visibility to single-key remaps
are follow-ups. Engine (108) and editor (88) C++ unit test suites pass; the editor UI
project compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Polish the "Condition" (Always / Alone) control added for single-key dual-key
remaps so it is production-ready:

- Localize the combo box: replace the hardcoded "Condition" / "Always" /
  "Alone (tap)" strings with x:Uid + resw entries (ConditionComboBox.Header,
  Condition_Always.Content, Condition_Alone.Content), matching the existing
  ElevationComboBox pattern.
- Gate visibility: the condition combo now shows only for a single-key remap
  whose action is a key/shortcut (the only case the engine supports "alone"),
  via UpdateConditionVisibility() driven off the trigger key count and action
  type. It starts collapsed and only toggles Visibility, so a condition set on
  load via SetCondition is preserved.
- Surface the condition in the list: add an "Alone" badge (localized
  AloneBadge.Text) to the single-key remap rows, bound to a new Remapping.IsAlone
  via BoolToVisibilityConverter, so alone remaps are distinguishable without
  opening the edit dialog.

Editor UI project compiles clean (0 warnings, 0 errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

@MasaYan24 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"
Contributor License Agreement

Contribution License Agreement

This Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
and conveys certain license rights to Microsoft Corporation and its affiliates (“Microsoft”) for Your
contributions to Microsoft open source projects. This Agreement is effective as of the latest signature
date below.

  1. Definitions.
    “Code” means the computer software code, whether in human-readable or machine-executable form,
    that is delivered by You to Microsoft under this Agreement.
    “Project” means any of the projects owned or managed by Microsoft and offered under a license
    approved by the Open Source Initiative (www.opensource.org).
    “Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any
    Project, including but not limited to communication on electronic mailing lists, source code control
    systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
    discussing and improving that Project, but excluding communication that is conspicuously marked or
    otherwise designated in writing by You as “Not a Submission.”
    “Submission” means the Code and any other copyrightable material Submitted by You, including any
    associated comments and documentation.
  2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any
    Project. This Agreement covers any and all Submissions that You, now or in the future (except as
    described in Section 4 below), Submit to any Project.
  3. Originality of Work. You represent that each of Your Submissions is entirely Your original work.
    Should You wish to Submit materials that are not Your original work, You may Submit them separately
    to the Project if You (a) retain all copyright and license information that was in the materials as You
    received them, (b) in the description accompanying Your Submission, include the phrase “Submission
    containing materials of a third party:” followed by the names of the third party and any licenses or other
    restrictions of which You are aware, and (c) follow any other instructions in the Project’s written
    guidelines concerning Submissions.
  4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else
    for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
    Submission is made in the course of Your work for an employer or Your employer has intellectual
    property rights in Your Submission by contract or applicable law, You must secure permission from Your
    employer to make the Submission before signing this Agreement. In that case, the term “You” in this
    Agreement will refer to You and the employer collectively. If You change employers in the future and
    desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
    and secure permission from the new employer before Submitting those Submissions.
  5. Licenses.
  • Copyright License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the
    Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute
    the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third
    parties.
  • Patent License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under
    Your patent claims that are necessarily infringed by the Submission or the combination of the
    Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
    import or otherwise dispose of the Submission alone or with the Project.
  • Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement.
    No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
    granted by implication, exhaustion, estoppel or otherwise.
  1. Representations and Warranties. You represent that You are legally entitled to grant the above
    licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
    have disclosed under Section 3). You represent that You have secured permission from Your employer to
    make the Submission in cases where Your Submission is made in the course of Your work for Your
    employer or Your employer has intellectual property rights in Your Submission by contract or applicable
    law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
    have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
    You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
    REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
    EXPRESSLY STATED IN SECTIONS 3, 4, AND 6, THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
    PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
    NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
  2. Notice to Microsoft. You agree to notify Microsoft in writing of any facts or circumstances of which
    You later become aware that would make Your representations in this Agreement inaccurate in any
    respect.
  3. Information about Submissions. You agree that contributions to Projects and information about
    contributions may be maintained indefinitely and disclosed publicly, including Your name and other
    information that You submit with Your Submission.
  4. Governing Law/Jurisdiction. This Agreement is governed by the laws of the State of Washington, and
    the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
    Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
    exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
    defenses of lack of personal jurisdiction and forum non-conveniens.
  5. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and
    supersedes any and all prior agreements, understandings or communications, written or oral, between
    the parties relating to the subject matter hereof. This Agreement may be assigned by Microsoft.

@niels9001

Copy link
Copy Markdown
Collaborator

@MasaYan24 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@MasaYan24 can you accept the policy agreement and would you mind taking a screenrecording of the new behavior?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds “tap-alone” (Karabiner-style to_if_alone) behavior for single-key remaps in Keyboard Manager, allowing a key to trigger an alternate action when tapped by itself while still behaving normally when used in combinations.

Changes:

  • Engine: adds a separate aloneSingleKeyReMap table and a new hook handler HandleSingleKeyAloneRemapEvent that defers decision until another key is pressed or the key is released.
  • Settings + interop: adds a "condition" field (always/alone) for single-key remaps and exposes parallel get/add/delete APIs for the alone table.
  • Editor UI: adds a localized “Condition” combo box (Always / Alone) and an “Alone” badge in the remap list.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp Resets alone-remap state between tests.
src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp Adds unit tests covering basic alone key behavior.
src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h Adds runtime tracking for pending vs combination alone keys + API surface.
src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp Implements alone runtime state helpers and alone remap lookup.
src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp Wires alone handler into the hook chain and includes alone table in “has remaps”.
src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.h Declares HandleSingleKeyAloneRemapEvent.
src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp Implements alone remap hook logic (lazy key-down, release-fire).
src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw Adds localized strings for Condition UI and Alone badge.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs Maps service-layer “IsAlone” into editor “Condition”.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs Loads/saves remap condition and passes it into SaveMapping.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml Adds Alone badge to remap list UI.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs Introduces SingleKeyRemapCondition + includes it in equality/hash.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs Tags service-returned mappings with IsAlone.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs Surfaces alone mappings and provides add/delete methods.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs Adds P/Invoke exports for alone remap APIs.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs Saves/deletes single-key mappings using condition to choose table.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs Stores condition and computes IsAlone for list badge binding.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs Adds condition getter/setter + visibility gating logic.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml Adds Condition combo box to the mapping UI.
src/modules/keyboardmanager/KeyboardManagerEditorLibraryWrapper/KeyboardManagerEditorLibraryWrapper.h Exposes alone remap APIs to the editor wrapper.
src/modules/keyboardmanager/KeyboardManagerEditorLibraryWrapper/KeyboardManagerEditorLibraryWrapper.cpp Implements alone remap enumeration/add/delete in wrapper.
src/modules/keyboardmanager/common/MappingConfiguration.h Adds alone remap table + APIs.
src/modules/keyboardmanager/common/MappingConfiguration.cpp Loads/saves alone condition via optional "condition" field and serializes alone table.
src/modules/keyboardmanager/common/KeyboardManagerConstants.h Adds "condition" setting constants (always/alone).
doc/devdocs/modules/keyboardmanager/keyboardeventhandlers.md Documents the new alone handler behavior and settings format.

Comment on lines +155 to +169
// Tapped alone: fire the remapped alone action as a tap (down + up).
std::vector<INPUT> keyEventList;
const auto& target = it->second;
if (target.index() == 0)
{
const DWORD targetKey = std::get<DWORD>(target);
// A disabled alone target means "tapping alone does nothing".
if (targetKey != CommonSharedConstants::VK_DISABLED)
{
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetKey), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
}
}
// NOTE: Shortcut- and text-valued alone targets are a later phase (no producer yet).

Comment on lines +113 to +118
// Step 1: a key-down of a DIFFERENT key while alone keys are pending means those pending keys
// are now being used in combination. Flush them by injecting their original key-down as a real
// key/modifier so the in-combination behavior (e.g. Right Ctrl acting as Ctrl) works.
if (isKeyDown)
{
for (const DWORD pendingKey : state.GetPendingAloneKeys())
Comment on lines +7 to 10
3. [HandleShortcutRemapEvent](#HandleShortcutRemapEvent)
3. [HandleOSLevelShortcutRemapEvent](#HandleOSLevelShortcutRemapEvent)
4. [HandleAppSpecificShortcutRemapEvent](#HandleAppSpecificShortcutRemapEvent)
5. [HandleSingleKeyToggleToModEvent (Obsolete))](#HandleSingleKeyToggleToModEvent-(Obsolete---Code-from-PoC-which-is-commented-out))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyboard Manager: Add support for single-key press detection, IME switching, and flexible remapping with modifier combinations

3 participants