Skip to content

Commit 5bfd94f

Browse files
committed
(#1393) Windows keyboard layout: determine parent terminal process and get the layout from it
1 parent bfbf2c4 commit 5bfd94f

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

PSReadLine/Keys.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,10 @@ internal static void TryGetCharFromConsoleKey(ConsoleKeyInfo key, ref char resul
156156
flags |= (1 << 2); /* If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer) */
157157
}
158158

159-
var kl = LoadKeyboardLayoutW("00000409", 0); // US English keyboard layout
160-
int charCount = ToUnicodeEx(virtualKey, scanCode, state, chars, chars.Length, flags, kl);
159+
var layout = WindowsKeyboardLayoutUtil.GetConsoleKeyboardLayout()
160+
?? WindowsKeyboardLayoutUtil.GetConsoleKeyboardLayoutFallback();
161+
162+
int charCount = ToUnicodeEx(virtualKey, scanCode, state, chars, chars.Length, flags, layout);
161163

162164
if (charCount == 1)
163165
{
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Microsoft.PowerShell
6+
{
7+
internal static class WindowsKeyboardLayoutUtil
8+
{
9+
/// <remarks>
10+
/// <para>
11+
/// This method helps to find the active keyboard layout in a terminal process that controls the current
12+
/// console application. The terminal process is not always the direct parent of the current process, but
13+
/// may be higher in the process tree in case PowerShell is a child of some other console process.
14+
/// </para>
15+
/// <para>
16+
/// Currently, we check up to 20 parent processes to see if their main window (as determined by the
17+
/// <see cref="Process.MainWindowHandle"/>) is visible.
18+
/// </para>
19+
/// <para>
20+
/// If this method returns <c>null</c>, it means it was unable to find the parent terminal process, and so
21+
/// you have to call the <see cref="GetConsoleKeyboardLayoutFallback"/>, which is known to not work properly
22+
/// in certain cases, as documented by https://github.com/PowerShell/PSReadLine/issues/1393
23+
/// </para>
24+
/// </remarks>
25+
public static IntPtr? GetConsoleKeyboardLayout()
26+
{
27+
// Define a limit not get stuck in case processed form a loop (possible in case pid reuse).
28+
const int iterationLimit = 20;
29+
30+
var pbi = new PROCESS_BASIC_INFORMATION();
31+
var process = Process.GetCurrentProcess();
32+
for (var i = 0; i < iterationLimit; ++i)
33+
{
34+
var isVisible = IsWindowVisible(process.MainWindowHandle);
35+
if (!isVisible)
36+
{
37+
// Main process window is invisible. This is not (likely) a terminal process.
38+
var status = NtQueryInformationProcess(process.Handle, 0, ref pbi, Marshal.SizeOf(pbi), out var _);
39+
if (status != 0 || pbi.InheritedFromUniqueProcessId == IntPtr.Zero)
40+
break;
41+
42+
try
43+
{
44+
process = Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32());
45+
}
46+
catch (Exception)
47+
{
48+
// No access to the process, or the process is already dead. Either way, we cannot determine its
49+
// keyboard layout.
50+
return null;
51+
}
52+
53+
continue;
54+
}
55+
56+
var tid = GetWindowThreadProcessId(process.MainWindowHandle, out _);
57+
if (tid == 0) return null;
58+
return GetKeyboardLayout(tid);
59+
}
60+
61+
return null;
62+
}
63+
64+
public static IntPtr GetConsoleKeyboardLayoutFallback()
65+
{
66+
return GetKeyboardLayout(0);
67+
}
68+
69+
[DllImport("User32.dll", SetLastError = true)]
70+
private static extern IntPtr GetKeyboardLayout(uint idThread);
71+
72+
[DllImport("Ntdll.dll")]
73+
static extern int NtQueryInformationProcess(
74+
IntPtr processHandle,
75+
int processInformationClass,
76+
ref PROCESS_BASIC_INFORMATION processInformation,
77+
int processInformationLength,
78+
out int returnLength);
79+
80+
[DllImport("user32.dll")]
81+
[return: MarshalAs(UnmanagedType.Bool)]
82+
static extern bool IsWindowVisible(IntPtr hWnd);
83+
84+
[DllImport("user32.dll", SetLastError = true)]
85+
static extern uint GetWindowThreadProcessId(IntPtr hwnd, out IntPtr proccess);
86+
87+
[StructLayout(LayoutKind.Sequential)]
88+
private struct PROCESS_BASIC_INFORMATION
89+
{
90+
internal IntPtr Reserved1;
91+
internal IntPtr PebBaseAddress;
92+
internal IntPtr Reserved2_0;
93+
internal IntPtr Reserved2_1;
94+
internal IntPtr UniqueProcessId;
95+
internal IntPtr InheritedFromUniqueProcessId;
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)