Skip to content

Fix various KKP encoding issues#20052

Open
lhecker wants to merge 6 commits intomainfrom
dev/lhecker/19977-kkp-altgr
Open

Fix various KKP encoding issues#20052
lhecker wants to merge 6 commits intomainfrom
dev/lhecker/19977-kkp-altgr

Conversation

@lhecker
Copy link
Copy Markdown
Member

@lhecker lhecker commented Apr 2, 2026

NOTE:
This commit changes our input model to treat all
Ctrl-Alt combinations as an AltGr modifier by default.
Ctrl+Alt+/ will not produce "\x1b\x1f" anymore.

This fixes the following issues:

  • Base-key report of non-ASCII keys was broken
  • AltGr on international keyboards would spam
    Ctrl/Alt up/down events repeatedly forever
  • KKP doesn't report modifier key repeats
  • AltGr+Key combinations were reported as Ctrl+Alt+key
    but should've been just plain Key-But-Modified

Closes #19977

Validation Steps Performed

This cannot be easily tested in unit tests, due to
the dependence on international keyboard layouts.
It appears to work correctly from what I can tell.

@github-actions

This comment has been minimized.

}

// Keep track of key repeats.
key.keyRepeat = _lastVirtualKeyCode == key.virtualKey;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Placing the detection outside a if (key.keyDown) block meant that a single key up/down would already be detected as a key repeat.

Comment on lines +293 to +296
if (const auto flags = _controlKeyStateFromVirtualKey(key.virtualKey, key.controlKeyState))
{
key.keyRepeat = (_previousControlKeyState & flags) != 0;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These 3 lines fix the AltGr key repeat.

Comment on lines +311 to +313
// Suppress modifier key events at all times - they aren't reported in any protocol.
(key.virtualKey >= VK_SHIFT && key.virtualKey <= VK_MENU) ||
(key.virtualKey >= VK_LSHIFT && key.virtualKey <= VK_RMENU) ||
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

KKP doesn't report key repeats for modifier keys.

(key.virtualKey >= VK_LSHIFT && key.virtualKey <= VK_RMENU) ||
(_kittyFlags != 0 ?
// If KKP is enabled, we only report repeats if ReportEventTypes is enabled.
WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes) :
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We also shouldn't report key repeats at all if the flag is off.

Comment on lines +377 to +380
key.altGrPressed = anyAltPressed && anyCtrlPressed;
key.ctrlPressed = key.altGrPressed ? (bothCtrlPressed || (rightAltPressed && leftCtrlIsReallyPressed)) : anyCtrlPressed;
key.altPressed = key.altGrPressed ? bothAltPressed : anyAltPressed;
key.shiftPressed = WI_IsFlagSet(key.controlKeyState, SHIFT_PRESSED);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

To make reliable AltGr detection possible throughout the code, I've made a bit of a more controversial change and removed the entire key.controlKeyState reliance throughout the code.

This crucially means that even the legacy key encoding will now treat AltGr distinct from other modifiers.

// NOTE: MSDN states that `lpSrcStr == lpDestStr` is valid for LCMAP_LOWERCASE.
len = LCMapStringW(LOCALE_INVARIANT, LCMAP_LOWERCASE, &buf[0], len, &buf[0], ARRAYSIZE(buf));
// NOTE: LCMapStringW returns the length including the null terminator.
len -= 1;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

len -= 1 was wrong and only applies to encoding null-terminated strings. I have switched to a length-based buffer a long time ago and missed fixing this. 🙈

Comment on lines +1417 to +1422
// In the context of KKP, AltGr acts more like a keyboard "layer" toggle.
// It's not a modifier that's ever transmitted as-is and instead modifies the actual base key code.
if (key.altGrPressed)
{
controlKeyState |= LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This new if condition ensures we treat AltGr like a layer shift that changes the base encoding of the key.

{ L"D Shift+Alt+a", L"\x1b[97;4u", D, true, 'A', 0x1E, L'A', Shift | Alt },
{ L"D Shift+a", L"A", D, true, 'A', 0x1E, L'A', Shift },
{ L"D Ctrl+a", L"\x1b[97;5u", D, true, 'A', 0x10, 0, Ctrl },
{ L"D Ctrl+Alt+a", L"æ", D, true, 'A', 0x10, L'æ', Ctrl | Alt },
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This has been rewritten to use French AZERTY events.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

all of them ??? is that safe? ... are we guaranteed that it exists on OneCore?

should we have tests in US-104 for the most part and allow a TestCase to specify a keyboard layout to set?

@github-actions

This comment has been minimized.

Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN.");

BYTE vkey = LOBYTE(OneCoreSafeVkKeyScanW(0));
BYTE vkey = LOBYTE(VkKeyScanExW(0, layout));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We still need to use OneCoreSaveVkKeyScanW; this test will fail to run (or, maybe the entire fixture will fail to load?) if we link VkKeyScanW.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

But it depends on LoadKeyboardLayout and ToUnicodeEx now. Won't all of these fail to work anyway? Do they have to work in the first? The TerminalInput logic is platform-independent with the exception of a single ToUnicodeEx call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay, so here's what I have come to understand.

We refer to these APIs using an extension APIset.

    ext-ms-win-ntuser-keyboard-l1-1-0
      GetKeyState
      GetKeyboardLayout
      MapVirtualKeyW
      ToUnicodeEx
      VkKeyScanW

OneCoreSafeEtc runs those through coniosrv or equivalent rather than ntuser.
In theory, we could have done some effort to compose onecore with a new extension apiset that fills the same contract. Instead we used runtime dynamic dispatch. Eh.

As long as the implementation calls OneCoreSafeEtc, we're fine.

We will need to prevent the kitty tests from running on OneCore, so that we do not experience test gate failures. :)

@lhecker
Copy link
Copy Markdown
Member Author

lhecker commented Apr 3, 2026

It is rather annoying that the /alternatename linker thing doesn't seem to work for x86.
And some of the KeyPressTests don't seem to work in CI which is extraordinarily annoying. This may mean that we have to either disable the KKP tests or remove them.

@DHowett
Copy link
Copy Markdown
Member

DHowett commented Apr 3, 2026

It is rather annoying that the /alternatename linker thing doesn't seem to work for x86. And some of the KeyPressTests don't seem to work in CI which is extraordinarily annoying. This may mean that we have to either disable the KKP tests or remove them.

Unfortunately, the deal with TestHook is that the no-op implementation must be in a separate .obj in the LIB-under-test. It cannot be an alternatename or some other weak definition kind of deal.

This type of test hook relies on the classic model of linking where objects are only pulled in even from libs if they are referred to for an unresolved symbol. TerminalInput.lib!TestHook.obj must be able to be skipped wholesale because it does not contribute an unresolved symbol to the test DLL.

@DHowett
Copy link
Copy Markdown
Member

DHowett commented Apr 3, 2026

(Whereas you have TestHook.obj in the UT project; you rather need TestHook.obj in TerminalInput which contains the no-op implementation; then you can implement the hook in the UT in whatever object file you want, including the test file that relies on the overridden hook)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broken input in fish shell

3 participants