2525
2626_STD_INPUT_HANDLE = _DWORD (- 10 )
2727_STD_OUTPUT_HANDLE = _DWORD (- 11 )
28+ # GetStdHandle returns INVALID_HANDLE_VALUE (-1) when no console is
29+ # attached (piped output, pythonw, services)
30+ _INVALID_HANDLE_VALUE = _HANDLE (- 1 ).value
31+
32+
33+ def _valid_handle (handle ) -> bool :
34+ return handle is not None and handle != _INVALID_HANDLE_VALUE
2835
2936
3037class WindowsConsoleModeFlags (enum .IntFlag ):
@@ -48,25 +55,33 @@ def __str__(self) -> str:
4855 return f'{ self .name } (0x{ self .value :04X} )'
4956
5057
58+ # Explicit argtypes are required: without them ctypes passes arguments
59+ # as 32-bit C ints, silently truncating 64-bit HANDLE values
5160_GetConsoleMode = _kernel32 .GetConsoleMode
61+ _GetConsoleMode .argtypes = (_HANDLE , ctypes .POINTER (_DWORD ))
5262_GetConsoleMode .restype = _BOOL
5363
5464_SetConsoleMode = _kernel32 .SetConsoleMode
65+ _SetConsoleMode .argtypes = (_HANDLE , _DWORD )
5566_SetConsoleMode .restype = _BOOL
5667
5768_GetStdHandle = _kernel32 .GetStdHandle
69+ _GetStdHandle .argtypes = (_DWORD ,)
5870_GetStdHandle .restype = _HANDLE
5971
60- _ReadConsoleInput = _kernel32 .ReadConsoleInputA
61- _ReadConsoleInput .restype = _BOOL
72+ _SetConsoleTextAttribute = _kernel32 .SetConsoleTextAttribute
73+ _SetConsoleTextAttribute .argtypes = (_HANDLE , _WORD )
74+ _SetConsoleTextAttribute .restype = _BOOL
6275
6376_h_console_input = _GetStdHandle (_STD_INPUT_HANDLE )
6477_input_mode = _DWORD ()
65- _GetConsoleMode (_HANDLE (_h_console_input ), ctypes .byref (_input_mode ))
78+ if _valid_handle (_h_console_input ):
79+ _GetConsoleMode (_HANDLE (_h_console_input ), ctypes .byref (_input_mode ))
6680
6781_h_console_output = _GetStdHandle (_STD_OUTPUT_HANDLE )
6882_output_mode = _DWORD ()
69- _GetConsoleMode (_HANDLE (_h_console_output ), ctypes .byref (_output_mode ))
83+ if _valid_handle (_h_console_output ):
84+ _GetConsoleMode (_HANDLE (_h_console_output ), ctypes .byref (_output_mode ))
7085
7186
7287class _COORD (ctypes .Structure ):
@@ -121,17 +136,34 @@ class _Event(ctypes.Union):
121136 _fields_ = (('EventType' , _WORD ), ('Event' , _Event ))
122137
123138
139+ _ReadConsoleInput = _kernel32 .ReadConsoleInputA
140+ _ReadConsoleInput .argtypes = (
141+ _HANDLE ,
142+ ctypes .POINTER (_INPUT_RECORD ),
143+ _DWORD ,
144+ ctypes .POINTER (_DWORD ),
145+ )
146+ _ReadConsoleInput .restype = _BOOL
147+
148+
124149def reset_console_mode () -> None :
125- _SetConsoleMode (_HANDLE (_h_console_input ), _DWORD (_input_mode .value ))
126- _SetConsoleMode (_HANDLE (_h_console_output ), _DWORD (_output_mode .value ))
150+ if _valid_handle (_h_console_input ):
151+ _SetConsoleMode (_HANDLE (_h_console_input ), _DWORD (_input_mode .value ))
152+
153+ if _valid_handle (_h_console_output ):
154+ _SetConsoleMode (_HANDLE (_h_console_output ), _DWORD (_output_mode .value ))
127155
128156
129157def set_console_mode () -> bool :
130- mode = (
131- _input_mode .value
132- | WindowsConsoleModeFlags .ENABLE_VIRTUAL_TERMINAL_INPUT
133- )
134- _SetConsoleMode (_HANDLE (_h_console_input ), _DWORD (mode ))
158+ if not _valid_handle (_h_console_output ):
159+ return False
160+
161+ if _valid_handle (_h_console_input ):
162+ mode = (
163+ _input_mode .value
164+ | WindowsConsoleModeFlags .ENABLE_VIRTUAL_TERMINAL_INPUT
165+ )
166+ _SetConsoleMode (_HANDLE (_h_console_input ), _DWORD (mode ))
135167
136168 mode = (
137169 _output_mode .value
@@ -146,7 +178,8 @@ def get_console_mode() -> int:
146178
147179
148180def set_text_color (color ) -> None :
149- _kernel32 .SetConsoleTextAttribute (_h_console_output , color )
181+ if _valid_handle (_h_console_output ):
182+ _SetConsoleTextAttribute (_HANDLE (_h_console_output ), _WORD (color ))
150183
151184
152185def print_color (text , color ) -> None :
@@ -156,19 +189,29 @@ def print_color(text, color) -> None:
156189
157190
158191def getch ():
192+ if not _valid_handle (_h_console_input ):
193+ return None
194+
159195 lp_buffer = (_INPUT_RECORD * 2 )()
160196 n_length = _DWORD (2 )
161197 lp_number_of_events_read = _DWORD ()
162198
163- _ReadConsoleInput (
199+ if not _ReadConsoleInput (
164200 _HANDLE (_h_console_input ),
165201 lp_buffer ,
166202 n_length ,
167203 ctypes .byref (lp_number_of_events_read ),
168- )
169-
170- char = lp_buffer [1 ].Event .KeyEvent .uChar .AsciiChar .decode ('ascii' )
171- if char == '\x00 ' :
204+ ):
172205 return None
173206
174- return char
207+ # Only the records that were actually read contain valid data, and
208+ # non-ASCII keys must not crash the decode
209+ for i in range (min (lp_number_of_events_read .value , len (lp_buffer ))):
210+ char = lp_buffer [i ].Event .KeyEvent .uChar .AsciiChar .decode (
211+ 'ascii' ,
212+ errors = 'replace' ,
213+ )
214+ if char != '\x00 ' :
215+ return char
216+
217+ return None
0 commit comments