@@ -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