1 """Light wrapper around the Win32 Console API - this module should only be imported on Windows
3 The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions
10 if sys
.platform
== "win32":
11 windll
= ctypes
.LibraryLoader(ctypes
.WinDLL
)
13 raise ImportError(f
"{__name__} can only be imported on Windows")
16 from ctypes
import Structure
, byref
, wintypes
17 from typing
import IO
, NamedTuple
, Type
, cast
19 from pip
._vendor
.rich
.color
import ColorSystem
20 from pip
._vendor
.rich
.style
import Style
23 ENABLE_VIRTUAL_TERMINAL_PROCESSING
= 4
25 COORD
= wintypes
._COORD
28 class LegacyWindowsError(Exception):
32 class WindowsCoordinates(NamedTuple
):
33 """Coordinates in the Windows Console API are (y, x), not (x, y).
34 This class is intended to prevent that confusion.
35 Rows and columns are indexed from 0.
36 This class can be used in place of wintypes._COORD in arguments and argtypes.
43 def from_param(cls
, value
: "WindowsCoordinates") -> COORD
:
44 """Converts a WindowsCoordinates into a wintypes _COORD structure.
45 This classmethod is internally called by ctypes to perform the conversion.
48 value (WindowsCoordinates): The input coordinates to convert.
51 wintypes._COORD: The converted coordinates struct.
53 return COORD(value
.col
, value
.row
)
56 class CONSOLE_SCREEN_BUFFER_INFO(Structure
):
59 ("dwCursorPosition", COORD
),
60 ("wAttributes", wintypes
.WORD
),
61 ("srWindow", wintypes
.SMALL_RECT
),
62 ("dwMaximumWindowSize", COORD
),
66 class CONSOLE_CURSOR_INFO(ctypes
.Structure
):
67 _fields_
= [("dwSize", wintypes
.DWORD
), ("bVisible", wintypes
.BOOL
)]
70 _GetStdHandle
= windll
.kernel32
.GetStdHandle
71 _GetStdHandle
.argtypes
= [
74 _GetStdHandle
.restype
= wintypes
.HANDLE
77 def GetStdHandle(handle
: int = STDOUT
) -> wintypes
.HANDLE
:
78 """Retrieves a handle to the specified standard device (standard input, standard output, or standard error).
81 handle (int): Integer identifier for the handle. Defaults to -11 (stdout).
84 wintypes.HANDLE: The handle
86 return cast(wintypes
.HANDLE
, _GetStdHandle(handle
))
89 _GetConsoleMode
= windll
.kernel32
.GetConsoleMode
90 _GetConsoleMode
.argtypes
= [wintypes
.HANDLE
, wintypes
.LPDWORD
]
91 _GetConsoleMode
.restype
= wintypes
.BOOL
94 def GetConsoleMode(std_handle
: wintypes
.HANDLE
) -> int:
95 """Retrieves the current input mode of a console's input buffer
96 or the current output mode of a console screen buffer.
99 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
102 LegacyWindowsError: If any error occurs while calling the Windows console API.
105 int: Value representing the current console mode as documented at
106 https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters
109 console_mode
= wintypes
.DWORD()
110 success
= bool(_GetConsoleMode(std_handle
, console_mode
))
112 raise LegacyWindowsError("Unable to get legacy Windows Console Mode")
113 return console_mode
.value
116 _FillConsoleOutputCharacterW
= windll
.kernel32
.FillConsoleOutputCharacterW
117 _FillConsoleOutputCharacterW
.argtypes
= [
121 cast(Type
[COORD
], WindowsCoordinates
),
122 ctypes
.POINTER(wintypes
.DWORD
),
124 _FillConsoleOutputCharacterW
.restype
= wintypes
.BOOL
127 def FillConsoleOutputCharacter(
128 std_handle
: wintypes
.HANDLE
,
131 start
: WindowsCoordinates
,
133 """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.
136 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
137 char (str): The character to write. Must be a string of length 1.
138 length (int): The number of times to write the character.
139 start (WindowsCoordinates): The coordinates to start writing at.
142 int: The number of characters written.
144 character
= ctypes
.c_char(char
.encode())
145 num_characters
= wintypes
.DWORD(length
)
146 num_written
= wintypes
.DWORD(0)
147 _FillConsoleOutputCharacterW(
154 return num_written
.value
157 _FillConsoleOutputAttribute
= windll
.kernel32
.FillConsoleOutputAttribute
158 _FillConsoleOutputAttribute
.argtypes
= [
162 cast(Type
[COORD
], WindowsCoordinates
),
163 ctypes
.POINTER(wintypes
.DWORD
),
165 _FillConsoleOutputAttribute
.restype
= wintypes
.BOOL
168 def FillConsoleOutputAttribute(
169 std_handle
: wintypes
.HANDLE
,
172 start
: WindowsCoordinates
,
174 """Sets the character attributes for a specified number of character cells,
175 beginning at the specified coordinates in a screen buffer.
178 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
179 attributes (int): Integer value representing the foreground and background colours of the cells.
180 length (int): The number of cells to set the output attribute of.
181 start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set.
184 int: The number of cells whose attributes were actually set.
186 num_cells
= wintypes
.DWORD(length
)
187 style_attrs
= wintypes
.WORD(attributes
)
188 num_written
= wintypes
.DWORD(0)
189 _FillConsoleOutputAttribute(
190 std_handle
, style_attrs
, num_cells
, start
, byref(num_written
)
192 return num_written
.value
195 _SetConsoleTextAttribute
= windll
.kernel32
.SetConsoleTextAttribute
196 _SetConsoleTextAttribute
.argtypes
= [
200 _SetConsoleTextAttribute
.restype
= wintypes
.BOOL
203 def SetConsoleTextAttribute(
204 std_handle
: wintypes
.HANDLE
, attributes
: wintypes
.WORD
206 """Set the colour attributes for all text written after this function is called.
209 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
210 attributes (int): Integer value representing the foreground and background colours.
214 bool: True if the attribute was set successfully, otherwise False.
216 return bool(_SetConsoleTextAttribute(std_handle
, attributes
))
219 _GetConsoleScreenBufferInfo
= windll
.kernel32
.GetConsoleScreenBufferInfo
220 _GetConsoleScreenBufferInfo
.argtypes
= [
222 ctypes
.POINTER(CONSOLE_SCREEN_BUFFER_INFO
),
224 _GetConsoleScreenBufferInfo
.restype
= wintypes
.BOOL
227 def GetConsoleScreenBufferInfo(
228 std_handle
: wintypes
.HANDLE
,
229 ) -> CONSOLE_SCREEN_BUFFER_INFO
:
230 """Retrieves information about the specified console screen buffer.
233 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
236 CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about
237 screen size, cursor position, colour attributes, and more."""
238 console_screen_buffer_info
= CONSOLE_SCREEN_BUFFER_INFO()
239 _GetConsoleScreenBufferInfo(std_handle
, byref(console_screen_buffer_info
))
240 return console_screen_buffer_info
243 _SetConsoleCursorPosition
= windll
.kernel32
.SetConsoleCursorPosition
244 _SetConsoleCursorPosition
.argtypes
= [
246 cast(Type
[COORD
], WindowsCoordinates
),
248 _SetConsoleCursorPosition
.restype
= wintypes
.BOOL
251 def SetConsoleCursorPosition(
252 std_handle
: wintypes
.HANDLE
, coords
: WindowsCoordinates
254 """Set the position of the cursor in the console screen
257 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
258 coords (WindowsCoordinates): The coordinates to move the cursor to.
261 bool: True if the function succeeds, otherwise False.
263 return bool(_SetConsoleCursorPosition(std_handle
, coords
))
266 _GetConsoleCursorInfo
= windll
.kernel32
.GetConsoleCursorInfo
267 _GetConsoleCursorInfo
.argtypes
= [
269 ctypes
.POINTER(CONSOLE_CURSOR_INFO
),
271 _GetConsoleCursorInfo
.restype
= wintypes
.BOOL
274 def GetConsoleCursorInfo(
275 std_handle
: wintypes
.HANDLE
, cursor_info
: CONSOLE_CURSOR_INFO
277 """Get the cursor info - used to get cursor visibility and width
280 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
281 cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct that receives information
282 about the console's cursor.
285 bool: True if the function succeeds, otherwise False.
287 return bool(_GetConsoleCursorInfo(std_handle
, byref(cursor_info
)))
290 _SetConsoleCursorInfo
= windll
.kernel32
.SetConsoleCursorInfo
291 _SetConsoleCursorInfo
.argtypes
= [
293 ctypes
.POINTER(CONSOLE_CURSOR_INFO
),
295 _SetConsoleCursorInfo
.restype
= wintypes
.BOOL
298 def SetConsoleCursorInfo(
299 std_handle
: wintypes
.HANDLE
, cursor_info
: CONSOLE_CURSOR_INFO
301 """Set the cursor info - used for adjusting cursor visibility and width
304 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
305 cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info.
308 bool: True if the function succeeds, otherwise False.
310 return bool(_SetConsoleCursorInfo(std_handle
, byref(cursor_info
)))
313 _SetConsoleTitle
= windll
.kernel32
.SetConsoleTitleW
314 _SetConsoleTitle
.argtypes
= [wintypes
.LPCWSTR
]
315 _SetConsoleTitle
.restype
= wintypes
.BOOL
318 def SetConsoleTitle(title
: str) -> bool:
319 """Sets the title of the current console window
322 title (str): The new title of the console window.
325 bool: True if the function succeeds, otherwise False.
327 return bool(_SetConsoleTitle(title
))
330 class LegacyWindowsTerm
:
331 """This class allows interaction with the legacy Windows Console API. It should only be used in the context
332 of environments where virtual terminal processing is not available. However, if it is used in a Windows environment,
333 the entire API should work.
336 file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout.
341 # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers
343 0, # black The Windows colours are defined in wincon.h as follows:
344 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001
345 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010
346 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100
347 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000
348 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000
349 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000
350 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000
351 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000
361 def __init__(self
, file: "IO[str]") -> None:
362 handle
= GetStdHandle(STDOUT
)
363 self
._handle
= handle
364 default_text
= GetConsoleScreenBufferInfo(handle
).wAttributes
365 self
._default
_text
= default_text
367 self
._default
_fore
= default_text
& 7
368 self
._default
_back
= (default_text
>> 4) & 7
369 self
._default
_attrs
= self
._default
_fore |
(self
._default
_back
<< 4)
372 self
.write
= file.write
373 self
.flush
= file.flush
376 def cursor_position(self
) -> WindowsCoordinates
:
377 """Returns the current position of the cursor (0-based)
380 WindowsCoordinates: The current cursor position.
382 coord
: COORD
= GetConsoleScreenBufferInfo(self
._handle
).dwCursorPosition
383 return WindowsCoordinates(row
=cast(int, coord
.Y
), col
=cast(int, coord
.X
))
386 def screen_size(self
) -> WindowsCoordinates
:
387 """Returns the current size of the console screen buffer, in character columns and rows
390 WindowsCoordinates: The width and height of the screen as WindowsCoordinates.
392 screen_size
: COORD
= GetConsoleScreenBufferInfo(self
._handle
).dwSize
393 return WindowsCoordinates(
394 row
=cast(int, screen_size
.Y
), col
=cast(int, screen_size
.X
)
397 def write_text(self
, text
: str) -> None:
398 """Write text directly to the terminal without any modification of styles
401 text (str): The text to write to the console
406 def write_styled(self
, text
: str, style
: Style
) -> None:
407 """Write styled text to the terminal.
410 text (str): The text to write
411 style (Style): The style of the text
414 bgcolor
= style
.bgcolor
416 color
, bgcolor
= bgcolor
, color
419 fore
= color
.downgrade(ColorSystem
.WINDOWS
).number
420 fore
= fore
if fore
is not None else 7 # Default to ANSI 7: White
422 fore
= fore | self
.BRIGHT_BIT
424 fore
= fore
& ~self
.BRIGHT_BIT
425 fore
= self
.ANSI_TO_WINDOWS
[fore
]
427 fore
= self
._default
_fore
430 back
= bgcolor
.downgrade(ColorSystem
.WINDOWS
).number
431 back
= back
if back
is not None else 0 # Default to ANSI 0: Black
432 back
= self
.ANSI_TO_WINDOWS
[back
]
434 back
= self
._default
_back
436 assert fore
is not None
437 assert back
is not None
439 SetConsoleTextAttribute(
440 self
._handle
, attributes
=ctypes
.c_ushort(fore |
(back
<< 4))
442 self
.write_text(text
)
443 SetConsoleTextAttribute(self
._handle
, attributes
=self
._default
_text
)
445 def move_cursor_to(self
, new_position
: WindowsCoordinates
) -> None:
446 """Set the position of the cursor
449 new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor.
451 if new_position
.col
< 0 or new_position
.row
< 0:
453 SetConsoleCursorPosition(self
._handle
, coords
=new_position
)
455 def erase_line(self
) -> None:
456 """Erase all content on the line the cursor is currently located at"""
457 screen_size
= self
.screen_size
458 cursor_position
= self
.cursor_position
459 cells_to_erase
= screen_size
.col
460 start_coordinates
= WindowsCoordinates(row
=cursor_position
.row
, col
=0)
461 FillConsoleOutputCharacter(
462 self
._handle
, " ", length
=cells_to_erase
, start
=start_coordinates
464 FillConsoleOutputAttribute(
467 length
=cells_to_erase
,
468 start
=start_coordinates
,
471 def erase_end_of_line(self
) -> None:
472 """Erase all content from the cursor position to the end of that line"""
473 cursor_position
= self
.cursor_position
474 cells_to_erase
= self
.screen_size
.col
- cursor_position
.col
475 FillConsoleOutputCharacter(
476 self
._handle
, " ", length
=cells_to_erase
, start
=cursor_position
478 FillConsoleOutputAttribute(
481 length
=cells_to_erase
,
482 start
=cursor_position
,
485 def erase_start_of_line(self
) -> None:
486 """Erase all content from the cursor position to the start of that line"""
487 row
, col
= self
.cursor_position
488 start
= WindowsCoordinates(row
, 0)
489 FillConsoleOutputCharacter(self
._handle
, " ", length
=col
, start
=start
)
490 FillConsoleOutputAttribute(
491 self
._handle
, self
._default
_attrs
, length
=col
, start
=start
494 def move_cursor_up(self
) -> None:
495 """Move the cursor up a single cell"""
496 cursor_position
= self
.cursor_position
497 SetConsoleCursorPosition(
499 coords
=WindowsCoordinates(
500 row
=cursor_position
.row
- 1, col
=cursor_position
.col
504 def move_cursor_down(self
) -> None:
505 """Move the cursor down a single cell"""
506 cursor_position
= self
.cursor_position
507 SetConsoleCursorPosition(
509 coords
=WindowsCoordinates(
510 row
=cursor_position
.row
+ 1,
511 col
=cursor_position
.col
,
515 def move_cursor_forward(self
) -> None:
516 """Move the cursor forward a single cell. Wrap to the next line if required."""
517 row
, col
= self
.cursor_position
518 if col
== self
.screen_size
.col
- 1:
523 SetConsoleCursorPosition(
524 self
._handle
, coords
=WindowsCoordinates(row
=row
, col
=col
)
527 def move_cursor_to_column(self
, column
: int) -> None:
528 """Move cursor to the column specified by the zero-based column index, staying on the same row
531 column (int): The zero-based column index to move the cursor to.
533 row
, _
= self
.cursor_position
534 SetConsoleCursorPosition(self
._handle
, coords
=WindowsCoordinates(row
, column
))
536 def move_cursor_backward(self
) -> None:
537 """Move the cursor backward a single cell. Wrap to the previous line if required."""
538 row
, col
= self
.cursor_position
541 col
= self
.screen_size
.col
- 1
544 SetConsoleCursorPosition(
545 self
._handle
, coords
=WindowsCoordinates(row
=row
, col
=col
)
548 def hide_cursor(self
) -> None:
549 """Hide the cursor"""
550 current_cursor_size
= self
._get
_cursor
_size
()
551 invisible_cursor
= CONSOLE_CURSOR_INFO(dwSize
=current_cursor_size
, bVisible
=0)
552 SetConsoleCursorInfo(self
._handle
, cursor_info
=invisible_cursor
)
554 def show_cursor(self
) -> None:
555 """Show the cursor"""
556 current_cursor_size
= self
._get
_cursor
_size
()
557 visible_cursor
= CONSOLE_CURSOR_INFO(dwSize
=current_cursor_size
, bVisible
=1)
558 SetConsoleCursorInfo(self
._handle
, cursor_info
=visible_cursor
)
560 def set_title(self
, title
: str) -> None:
561 """Set the title of the terminal window
564 title (str): The new title of the console window
566 assert len(title
) < 255, "Console title must be less than 255 characters"
567 SetConsoleTitle(title
)
569 def _get_cursor_size(self
) -> int:
570 """Get the percentage of the character cell that is filled by the cursor"""
571 cursor_info
= CONSOLE_CURSOR_INFO()
572 GetConsoleCursorInfo(self
._handle
, cursor_info
=cursor_info
)
573 return int(cursor_info
.dwSize
)
576 if __name__
== "__main__":
577 handle
= GetStdHandle()
579 from pip
._vendor
.rich
.console
import Console
583 term
= LegacyWindowsTerm(sys
.stdout
)
584 term
.set_title("Win32 Console Examples")
586 style
= Style(color
="black", bgcolor
="red")
588 heading
= Style
.parse("black on green")
590 # Check colour output
591 console
.rule("Checking colour output")
592 console
.print("[on red]on red!")
593 console
.print("[blue]blue!")
594 console
.print("[yellow]yellow!")
595 console
.print("[bold yellow]bold yellow!")
596 console
.print("[bright_yellow]bright_yellow!")
597 console
.print("[dim bright_yellow]dim bright_yellow!")
598 console
.print("[italic cyan]italic cyan!")
599 console
.print("[bold white on blue]bold white on blue!")
600 console
.print("[reverse bold white on blue]reverse bold white on blue!")
601 console
.print("[bold black on cyan]bold black on cyan!")
602 console
.print("[black on green]black on green!")
603 console
.print("[blue on green]blue on green!")
604 console
.print("[white on black]white on black!")
605 console
.print("[black on white]black on white!")
606 console
.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!")
608 # Check cursor movement
609 console
.rule("Checking cursor movement")
611 term
.move_cursor_backward()
612 term
.move_cursor_backward()
613 term
.write_text("went back and wrapped to prev line")
615 term
.move_cursor_up()
616 term
.write_text("we go up")
618 term
.move_cursor_down()
619 term
.write_text("and down")
621 term
.move_cursor_up()
622 term
.move_cursor_backward()
623 term
.move_cursor_backward()
624 term
.write_text("we went up and back 2")
626 term
.move_cursor_down()
627 term
.move_cursor_backward()
628 term
.move_cursor_backward()
629 term
.write_text("we went down and back 2")
632 # Check erasing of lines
635 console
.rule("Checking line erasing")
636 console
.print("\n...Deleting to the start of the line...")
637 term
.write_text("The red arrow shows the cursor location, and direction of erase")
639 term
.move_cursor_to_column(16)
640 term
.write_styled("<", Style
.parse("black on red"))
641 term
.move_cursor_backward()
643 term
.erase_start_of_line()
646 console
.print("\n\n...And to the end of the line...")
647 term
.write_text("The red arrow shows the cursor location, and direction of erase")
650 term
.move_cursor_to_column(16)
651 term
.write_styled(">", Style
.parse("black on red"))
653 term
.erase_end_of_line()
656 console
.print("\n\n...Now the whole line will be erased...")
657 term
.write_styled("I'm going to disappear!", style
=Style
.parse("black on cyan"))