Skip to content

Commit ff4bbd5

Browse files
authored
Windows keyboard layout handling: get the current layout from the parent terminal process (#3786)
1 parent 5e9ea88 commit ff4bbd5

File tree

2 files changed

+108
-3
lines changed

2 files changed

+108
-3
lines changed

PSReadLine/Keys.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ public override int GetHashCode()
115115
public static extern uint MapVirtualKey(ConsoleKey uCode, uint uMapType);
116116

117117
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
118-
public static extern int ToUnicode(
118+
public static extern int ToUnicodeEx(
119119
ConsoleKey uVirtKey,
120120
uint uScanCode,
121121
byte[] lpKeyState,
122122
[MarshalAs(UnmanagedType.LPArray)] [Out] char[] chars,
123123
int charMaxCount,
124-
uint flags);
124+
uint flags,
125+
IntPtr dwhkl);
125126

126127
static readonly ThreadLocal<char[]> toUnicodeBuffer = new ThreadLocal<char[]>(() => new char[2]);
127128
static readonly ThreadLocal<byte[]> toUnicodeStateBuffer = new ThreadLocal<byte[]>(() => new byte[256]);
@@ -147,7 +148,9 @@ internal static void TryGetCharFromConsoleKey(ConsoleKeyInfo key, ref char resul
147148
{
148149
flags |= (1 << 2); /* If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer) */
149150
}
150-
int charCount = ToUnicode(virtualKey, scanCode, state, chars, chars.Length, flags);
151+
152+
IntPtr layout = PlatformWindows.GetConsoleKeyboardLayout();
153+
int charCount = ToUnicodeEx(virtualKey, scanCode, state, chars, chars.Length, flags, layout);
151154

152155
if (charCount == 1)
153156
{

PSReadLine/PlatformWindows.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ internal static IConsole OneTimeInit(PSConsoleReadLine singleton)
140140
var breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(OnBreak));
141141
SetConsoleCtrlHandler((BreakHandler)breakHandlerGcHandle.Target, true);
142142
_enableVtOutput = !Console.IsOutputRedirected && SetConsoleOutputVirtualTerminalProcessing();
143+
_terminalOwnerThreadId = GetTerminalOwnerThreadId();
143144

144145
return _enableVtOutput ? new VirtualTerminal() : new LegacyWin32Console();
145146
}
@@ -1015,4 +1016,105 @@ private static void TerminateStragglers()
10151016
}
10161017
}
10171018
}
1019+
1020+
private static uint _terminalOwnerThreadId;
1021+
1022+
/// <remarks>
1023+
/// This method helps to find the owner thread of the terminal window used by this pwsh instance,
1024+
/// by looking for a parent process whose <see cref="Process.MainWindowHandle"/>) is visible.
1025+
///
1026+
/// The terminal process is not always the direct parent of the current process, but may be higher
1027+
/// in the process tree in case this pwsh process is a child of some other console process.
1028+
///
1029+
/// This works well in Windows Terminal (with profile), IntelliJ and VSCode.
1030+
/// It doesn't work when PowerShell runs in conhost, or when it gets started from Start Menu with
1031+
/// Windows Terminal as the default terminal application (without profile).
1032+
/// </remarks>
1033+
private static uint GetTerminalOwnerThreadId()
1034+
{
1035+
try
1036+
{
1037+
// The window handle returned by `GetConsoleWindow` is not the correct terminal/console window for us
1038+
// to query about the keyboard layout change. It's the window created for a console application, such
1039+
// as `cmd` or `pwsh`, so its owner process in those cases will be `cmd` or `pwsh`.
1040+
//
1041+
// When we are running with conhost, this window is visible, but it's not what we want and needs to be
1042+
// filtered out. When running with conhost, we want the window owned by the conhost. But unfortunately,
1043+
// there is no reliable way to get the conhost process that is associated with the current pwsh, since
1044+
// it's not in the parent chain of the process tree.
1045+
// So, this method is supposed to always fail when running with conhost.
1046+
IntPtr wrongHandle = GetConsoleWindow();
1047+
1048+
// Limit for parent process walk-up for not getting stuck in a loop (possible in case pid reuse).
1049+
const int iterationLimit = 20;
1050+
var process = Process.GetCurrentProcess();
1051+
1052+
for (int i = 0; i < iterationLimit; ++i)
1053+
{
1054+
if (process.ProcessName is "explorer")
1055+
{
1056+
// We've reached the root of the process tree. This can happen when PowerShell was started
1057+
// from Start Menu with Windows Terminal as the default terminal application.
1058+
// The `explorer` process has a visible window, but it doesn't help for getting the layout
1059+
// change. Again, we need to find the terminal window owner.
1060+
break;
1061+
}
1062+
1063+
IntPtr mainWindowHandle = process.MainWindowHandle;
1064+
if (mainWindowHandle == wrongHandle)
1065+
{
1066+
// This can only happen when we are running with conhost.
1067+
// Break early because the terminal owner process is not in the parent chain in this scenario.
1068+
break;
1069+
}
1070+
1071+
if (mainWindowHandle != IntPtr.Zero && IsWindowVisible(mainWindowHandle))
1072+
{
1073+
// The window is visible, so it's likely the terminal window.
1074+
return GetWindowThreadProcessId(process.MainWindowHandle, out _);
1075+
}
1076+
1077+
// When reaching here, the main window of the process:
1078+
// - doesn't exist, or
1079+
// - exists but invisible
1080+
// So, this is likely not a terminal process.
1081+
// Now we get its parent process and continue with the check.
1082+
int parentId = GetParentPid(process);
1083+
process = Process.GetProcessById(parentId);
1084+
}
1085+
}
1086+
catch (Exception)
1087+
{
1088+
// No access to the process, or the process is already dead.
1089+
// Either way, we cannot determine the owner thread of the terminal window.
1090+
}
1091+
1092+
// We could not find the owner thread/process of the terminal window in following scenarios:
1093+
// 1. pwsh is running with conhost.
1094+
// This happens when conhost is set as the default terminal application, and a user starts pwsh
1095+
// from the Start Menu, or with `win+r` (run code) and etc.
1096+
//
1097+
// 2. pwsh is running with Windows Terminal, but was not started from a Windows Terminal profile.
1098+
// This happens when Windows Terminal is set as the default terminal application, and a user
1099+
// starts pwsh from the Start Menu, or with `win+r` (run code) and etc.
1100+
// The `WindowsTerminal` process is not in the parent process chain in this case.
1101+
//
1102+
// 3. pwsh's parent process chain is broken -- a parent was terminated so we cannot walk up the chain.
1103+
return 0;
1104+
}
1105+
1106+
internal static IntPtr GetConsoleKeyboardLayout()
1107+
{
1108+
return GetKeyboardLayout(_terminalOwnerThreadId);
1109+
}
1110+
1111+
[DllImport("user32.dll")]
1112+
[return: MarshalAs(UnmanagedType.Bool)]
1113+
private static extern bool IsWindowVisible(IntPtr hWnd);
1114+
1115+
[DllImport("User32.dll", SetLastError = true)]
1116+
private static extern IntPtr GetKeyboardLayout(uint idThread);
1117+
1118+
[DllImport("user32.dll", SetLastError = true)]
1119+
private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint proccess);
10181120
}

0 commit comments

Comments
 (0)