@@ -140,6 +140,7 @@ internal static IConsole OneTimeInit(PSConsoleReadLine singleton)
140
140
var breakHandlerGcHandle = GCHandle . Alloc ( new BreakHandler ( OnBreak ) ) ;
141
141
SetConsoleCtrlHandler ( ( BreakHandler ) breakHandlerGcHandle . Target , true ) ;
142
142
_enableVtOutput = ! Console . IsOutputRedirected && SetConsoleOutputVirtualTerminalProcessing ( ) ;
143
+ _terminalOwnerThreadId = GetTerminalOwnerThreadId ( ) ;
143
144
144
145
return _enableVtOutput ? new VirtualTerminal ( ) : new LegacyWin32Console ( ) ;
145
146
}
@@ -1015,4 +1016,105 @@ private static void TerminateStragglers()
1015
1016
}
1016
1017
}
1017
1018
}
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 ) ;
1018
1120
}
0 commit comments