Skip to content

Fix various KKP encoding issues - #20052

Merged
Dustin L. Howett (DHowett) merged 10 commits into
mainfrom
dev/lhecker/19977-kkp-altgr
Jul 31, 2026
Merged

Dustin L. Howett (DHowett) merged 10 commits into
mainfrom
dev/lhecker/19977-kkp-altgr

Conversation

@lhecker

@lhecker Leonard Hecker (lhecker) commented Apr 2, 2026 •

Copy link
Copy Markdown
Member

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 thread src/terminal/input/terminalInput.cpp Resolved
Comment thread src/terminal/input/terminalInput.cpp Resolved
Comment thread src/terminal/input/terminalInput.cpp Outdated
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?

@lhecker Leonard Hecker (lhecker) Apr 17, 2026 •

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.

all of them ??? is that safe?

Yes and yes.

are we guaranteed that it exists on OneCore?

No. But all of this code relies on APIs that are categorically unavailable in OneCore, so it probably doesn't matter.

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

That's how it works now, via SetTerminalInputKeyboardLayout.

@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. :)

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.

I have added exception guards around SetTerminalInputKeyboardLayout. However, I have not checked if it results in linking failures in OneCore. I'm somewhat confused how this works in general given that we have never defined a OneCoreSafe wrapper for ToUnicodeEx.

@lhecker

Copy link
Copy Markdown
Member Author

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

Dustin L. Howett (DHowett) commented Apr 3, 2026 •

Copy link
Copy Markdown
Member

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

Dustin L. Howett (DHowett) commented Apr 3, 2026 •

Copy link
Copy Markdown
Member

(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)

@github-actions

This comment has been minimized.

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.

OK, I think I understand this now. Thanks for being patient. Let's get it in Canary and see what happens.

@DHowett
Dustin L. Howett (DHowett) merged commit fbda436 into main Jul 31, 2026
20 checks passed
@DHowett
Dustin L. Howett (DHowett) deleted the dev/lhecker/19977-kkp-altgr branch July 31, 2026 23:14
@github-project-automation github-project-automation Bot moved this to To Cherry Pick in 1.25 Servicing Pipeline Sep 8, 2026
@DHowett Dustin L. Howett (DHowett) moved this from To Cherry Pick to Cherry Picked in 1.25 Servicing Pipeline Sep 8, 2026
Dustin L. Howett (DHowett) pushed a commit that referenced this pull request Sep 8, 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.

(cherry picked from commit fbda436)
Service-Card-Id: PVTI_lADOAF3p4s4BQX0-zg6CmdI
Service-Version: 1.25
Bisma (bismawy) pushed a commit to bismawy/pi-arnative that referenced this pull request Sep 28, 2026
Windows Terminal >= 1.24 aliases every Ctrl+Alt combo to AltGr
(microsoft/terminal#20052), so the legacy "ESC + control byte" pair that
matchesKey() looks for (ctrl+alt+r = \x1b\x12) is never emitted there and the
keystroke dies inside the terminal. WSL panes inherit that input translation,
so they need the same treatment; pi's own useWindowsKeybindings() draws the
line in the same place (it is not exported from the package root, so the check
is mirrored).

- lib/shortcuts.ts: one place for the three shortcuts (t/n/r). Windows-like
  platforms register ctrl+alt+<letter> and alt+<letter>; everyone else keeps
  ctrl+alt only (macOS: plain Option composes characters). The cheatsheet label
  is derived from the key id that is actually registered, so a tab can no
  longer advertise a key the platform never binds; on macOS it renders
  "Ctrl+option+r" like pi's own formatter does.
- extensions/section-headers.ts registers every variant through
  pi.registerShortcut; extensions/footer.ts matches through the same object.
  Both build it once at module scope (matches() runs per keystroke).
- /reload still intercepts handleInput on purpose: ExtensionContext.reload()
  exists in the typings but the context handed to shortcut handlers does not
  implement it.
- README: platform notes (Windows/WSL, macOS, Linux GNOME owning ctrl+alt+t)
  and PI_TUI_ESC_TIMEOUT for the intermittent "Escape + stray letter" glitch —
  pi flushes a lone ESC after ~10 ms, which splits ESC-prefixed shortcuts on
  slow or multiplexed PTYs (verified: 20 ms gap -> ["\x1b","r"], Escape fired;
  100 ms -> ["\x1br"], shortcut fired).

Verified: npm test 10/10 (incl. new lib/shortcuts.ts self-check), platform
matrix forced for win32 / WSL / linux / darwin, live registration probe
["ctrl+alt+t","alt+t"] and cheatsheet [Alt+t]/[Alt+n]/[Alt+r] on win32.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Broken input in fish shell

3 participants