codekingpro/portable-devtools
114k
1"""Light wrapper around the Win32 Console API - this module should only be imported on Windows2 3The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions4"""5 6import ctypes7import sys8from typing import Any9 10windll: Any = None11if sys.platform == "win32":12 windll = ctypes.LibraryLoader(ctypes.WinDLL)13else:14 raise ImportError(f"{__name__} can only be imported on Windows")15 16import time17from ctypes import Structure, byref, wintypes18from typing import IO, NamedTuple, Type, cast19 20from rich.color import ColorSystem21from rich.style import Style22 23STDOUT = -1124ENABLE_VIRTUAL_TERMINAL_PROCESSING = 425 26COORD = wintypes._COORD27 28 29class LegacyWindowsError(Exception):30 pass31 32 33class WindowsCoordinates(NamedTuple):34 """Coordinates in the Windows Console API are (y, x), not (x, y).35 This class is intended to prevent that confusion.36 Rows and columns are indexed from 0.37 This class can be used in place of wintypes._COORD in arguments and argtypes.38 """39 40 row: int41 col: int42 43 @classmethod44 def from_param(cls, value: "WindowsCoordinates") -> COORD:45 """Converts a WindowsCoordinates into a wintypes _COORD structure.46 This classmethod is internally called by ctypes to perform the conversion.47 48 Args:49 value (WindowsCoordinates): The input coordinates to convert.50 51 Returns:52 wintypes._COORD: The converted coordinates struct.53 """54 return COORD(value.col, value.row)55 56 57class CONSOLE_SCREEN_BUFFER_INFO(Structure):58 _fields_ = [59 ("dwSize", COORD),60 ("dwCursorPosition", COORD),61 ("wAttributes", wintypes.WORD),62 ("srWindow", wintypes.SMALL_RECT),63 ("dwMaximumWindowSize", COORD),64 ]65 66 67class CONSOLE_CURSOR_INFO(ctypes.Structure):68 _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)]69 70 71_GetStdHandle = windll.kernel32.GetStdHandle72_GetStdHandle.argtypes = [73 wintypes.DWORD,74]75_GetStdHandle.restype = wintypes.HANDLE76 77 78def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE:79 """Retrieves a handle to the specified standard device (standard input, standard output, or standard error).80 81 Args:82 handle (int): Integer identifier for the handle. Defaults to -11 (stdout).83 84 Returns:85 wintypes.HANDLE: The handle86 """87 return cast(wintypes.HANDLE, _GetStdHandle(handle))88 89 90_GetConsoleMode = windll.kernel32.GetConsoleMode91_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]92_GetConsoleMode.restype = wintypes.BOOL93 94 95def GetConsoleMode(std_handle: wintypes.HANDLE) -> int:96 """Retrieves the current input mode of a console's input buffer97 or the current output mode of a console screen buffer.98 99 Args:100 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.101 102 Raises:103 LegacyWindowsError: If any error occurs while calling the Windows console API.104 105 Returns:106 int: Value representing the current console mode as documented at107 https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters108 """109 110 console_mode = wintypes.DWORD()111 success = bool(_GetConsoleMode(std_handle, console_mode))112 if not success:113 raise LegacyWindowsError("Unable to get legacy Windows Console Mode")114 return console_mode.value115 116 117_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW118_FillConsoleOutputCharacterW.argtypes = [119 wintypes.HANDLE,120 ctypes.c_char,121 wintypes.DWORD,122 cast(Type[COORD], WindowsCoordinates),123 ctypes.POINTER(wintypes.DWORD),124]125_FillConsoleOutputCharacterW.restype = wintypes.BOOL126 127 128def FillConsoleOutputCharacter(129 std_handle: wintypes.HANDLE,130 char: str,131 length: int,132 start: WindowsCoordinates,133) -> int:134 """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.135 136 Args:137 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.138 char (str): The character to write. Must be a string of length 1.139 length (int): The number of times to write the character.140 start (WindowsCoordinates): The coordinates to start writing at.141 142 Returns:143 int: The number of characters written.144 """145 character = ctypes.c_char(char.encode())146 num_characters = wintypes.DWORD(length)147 num_written = wintypes.DWORD(0)148 _FillConsoleOutputCharacterW(149 std_handle,150 character,151 num_characters,152 start,153 byref(num_written),154 )155 return num_written.value156 157 158_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute159_FillConsoleOutputAttribute.argtypes = [160 wintypes.HANDLE,161 wintypes.WORD,162 wintypes.DWORD,163 cast(Type[COORD], WindowsCoordinates),164 ctypes.POINTER(wintypes.DWORD),165]166_FillConsoleOutputAttribute.restype = wintypes.BOOL167 168 169def FillConsoleOutputAttribute(170 std_handle: wintypes.HANDLE,171 attributes: int,172 length: int,173 start: WindowsCoordinates,174) -> int:175 """Sets the character attributes for a specified number of character cells,176 beginning at the specified coordinates in a screen buffer.177 178 Args:179 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.180 attributes (int): Integer value representing the foreground and background colours of the cells.181 length (int): The number of cells to set the output attribute of.182 start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set.183 184 Returns:185 int: The number of cells whose attributes were actually set.186 """187 num_cells = wintypes.DWORD(length)188 style_attrs = wintypes.WORD(attributes)189 num_written = wintypes.DWORD(0)190 _FillConsoleOutputAttribute(191 std_handle, style_attrs, num_cells, start, byref(num_written)192 )193 return num_written.value194 195 196_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute197_SetConsoleTextAttribute.argtypes = [198 wintypes.HANDLE,199 wintypes.WORD,200]201_SetConsoleTextAttribute.restype = wintypes.BOOL202 203 204def SetConsoleTextAttribute(205 std_handle: wintypes.HANDLE, attributes: wintypes.WORD206) -> bool:207 """Set the colour attributes for all text written after this function is called.208 209 Args:210 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.211 attributes (int): Integer value representing the foreground and background colours.212 213 214 Returns:215 bool: True if the attribute was set successfully, otherwise False.216 """217 return bool(_SetConsoleTextAttribute(std_handle, attributes))218 219 220_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo221_GetConsoleScreenBufferInfo.argtypes = [222 wintypes.HANDLE,223 ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),224]225_GetConsoleScreenBufferInfo.restype = wintypes.BOOL226 227 228def GetConsoleScreenBufferInfo(229 std_handle: wintypes.HANDLE,230) -> CONSOLE_SCREEN_BUFFER_INFO:231 """Retrieves information about the specified console screen buffer.232 233 Args:234 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.235 236 Returns:237 CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about238 screen size, cursor position, colour attributes, and more."""239 console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO()240 _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info))241 return console_screen_buffer_info242 243 244_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition245_SetConsoleCursorPosition.argtypes = [246 wintypes.HANDLE,247 cast(Type[COORD], WindowsCoordinates),248]249_SetConsoleCursorPosition.restype = wintypes.BOOL250 251 252def SetConsoleCursorPosition(253 std_handle: wintypes.HANDLE, coords: WindowsCoordinates254) -> bool:255 """Set the position of the cursor in the console screen256 257 Args:258 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.259 coords (WindowsCoordinates): The coordinates to move the cursor to.260 261 Returns:262 bool: True if the function succeeds, otherwise False.263 """264 return bool(_SetConsoleCursorPosition(std_handle, coords))265 266 267_GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo268_GetConsoleCursorInfo.argtypes = [269 wintypes.HANDLE,270 ctypes.POINTER(CONSOLE_CURSOR_INFO),271]272_GetConsoleCursorInfo.restype = wintypes.BOOL273 274 275def GetConsoleCursorInfo(276 std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO277) -> bool:278 """Get the cursor info - used to get cursor visibility and width279 280 Args:281 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.282 cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct that receives information283 about the console's cursor.284 285 Returns:286 bool: True if the function succeeds, otherwise False.287 """288 return bool(_GetConsoleCursorInfo(std_handle, byref(cursor_info)))289 290 291_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo292_SetConsoleCursorInfo.argtypes = [293 wintypes.HANDLE,294 ctypes.POINTER(CONSOLE_CURSOR_INFO),295]296_SetConsoleCursorInfo.restype = wintypes.BOOL297 298 299def SetConsoleCursorInfo(300 std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO301) -> bool:302 """Set the cursor info - used for adjusting cursor visibility and width303 304 Args:305 std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.306 cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info.307 308 Returns:309 bool: True if the function succeeds, otherwise False.310 """311 return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info)))312 313 314_SetConsoleTitle = windll.kernel32.SetConsoleTitleW315_SetConsoleTitle.argtypes = [wintypes.LPCWSTR]316_SetConsoleTitle.restype = wintypes.BOOL317 318 319def SetConsoleTitle(title: str) -> bool:320 """Sets the title of the current console window321 322 Args:323 title (str): The new title of the console window.324 325 Returns:326 bool: True if the function succeeds, otherwise False.327 """328 return bool(_SetConsoleTitle(title))329 330 331class LegacyWindowsTerm:332 """This class allows interaction with the legacy Windows Console API. It should only be used in the context333 of environments where virtual terminal processing is not available. However, if it is used in a Windows environment,334 the entire API should work.335 336 Args:337 file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout.338 """339 340 BRIGHT_BIT = 8341 342 # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers343 ANSI_TO_WINDOWS = [344 0, # black The Windows colours are defined in wincon.h as follows:345 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001346 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010347 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100348 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000349 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000350 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000351 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000352 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000353 12, # bright red354 10, # bright green355 14, # bright yellow356 9, # bright blue357 13, # bright magenta358 11, # bright cyan359 15, # bright white360 ]361 362 def __init__(self, file: "IO[str]") -> None:363 handle = GetStdHandle(STDOUT)364 self._handle = handle365 default_text = GetConsoleScreenBufferInfo(handle).wAttributes366 self._default_text = default_text367 368 self._default_fore = default_text & 7369 self._default_back = (default_text >> 4) & 7370 self._default_attrs = self._default_fore | (self._default_back << 4)371 372 self._file = file373 self.write = file.write374 self.flush = file.flush375 376 @property377 def cursor_position(self) -> WindowsCoordinates:378 """Returns the current position of the cursor (0-based)379 380 Returns:381 WindowsCoordinates: The current cursor position.382 """383 coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition384 return WindowsCoordinates(row=coord.Y, col=coord.X)385 386 @property387 def screen_size(self) -> WindowsCoordinates:388 """Returns the current size of the console screen buffer, in character columns and rows389 390 Returns:391 WindowsCoordinates: The width and height of the screen as WindowsCoordinates.392 """393 screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize394 return WindowsCoordinates(row=screen_size.Y, col=screen_size.X)395 396 def write_text(self, text: str) -> None:397 """Write text directly to the terminal without any modification of styles398 399 Args:400 text (str): The text to write to the console401 """402 self.write(text)403 self.flush()404 405 def write_styled(self, text: str, style: Style) -> None:406 """Write styled text to the terminal.407 408 Args:409 text (str): The text to write410 style (Style): The style of the text411 """412 color = style.color413 bgcolor = style.bgcolor414 if style.reverse:415 color, bgcolor = bgcolor, color416 417 if color:418 fore = color.downgrade(ColorSystem.WINDOWS).number419 fore = fore if fore is not None else 7 # Default to ANSI 7: White420 if style.bold:421 fore = fore | self.BRIGHT_BIT422 if style.dim:423 fore = fore & ~self.BRIGHT_BIT424 fore = self.ANSI_TO_WINDOWS[fore]425 else:426 fore = self._default_fore427 428 if bgcolor:429 back = bgcolor.downgrade(ColorSystem.WINDOWS).number430 back = back if back is not None else 0 # Default to ANSI 0: Black431 back = self.ANSI_TO_WINDOWS[back]432 else:433 back = self._default_back434 435 assert fore is not None436 assert back is not None437 438 SetConsoleTextAttribute(439 self._handle, attributes=ctypes.c_ushort(fore | (back << 4))440 )441 self.write_text(text)442 SetConsoleTextAttribute(self._handle, attributes=self._default_text)443 444 def move_cursor_to(self, new_position: WindowsCoordinates) -> None:445 """Set the position of the cursor446 447 Args:448 new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor.449 """450 if new_position.col < 0 or new_position.row < 0:451 return452 SetConsoleCursorPosition(self._handle, coords=new_position)453 454 def erase_line(self) -> None:455 """Erase all content on the line the cursor is currently located at"""456 screen_size = self.screen_size457 cursor_position = self.cursor_position458 cells_to_erase = screen_size.col459 start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0)460 FillConsoleOutputCharacter(461 self._handle, " ", length=cells_to_erase, start=start_coordinates462 )463 FillConsoleOutputAttribute(464 self._handle,465 self._default_attrs,466 length=cells_to_erase,467 start=start_coordinates,468 )469 470 def erase_end_of_line(self) -> None:471 """Erase all content from the cursor position to the end of that line"""472 cursor_position = self.cursor_position473 cells_to_erase = self.screen_size.col - cursor_position.col474 FillConsoleOutputCharacter(475 self._handle, " ", length=cells_to_erase, start=cursor_position476 )477 FillConsoleOutputAttribute(478 self._handle,479 self._default_attrs,480 length=cells_to_erase,481 start=cursor_position,482 )483 484 def erase_start_of_line(self) -> None:485 """Erase all content from the cursor position to the start of that line"""486 row, col = self.cursor_position487 start = WindowsCoordinates(row, 0)488 FillConsoleOutputCharacter(self._handle, " ", length=col, start=start)489 FillConsoleOutputAttribute(490 self._handle, self._default_attrs, length=col, start=start491 )492 493 def move_cursor_up(self) -> None:494 """Move the cursor up a single cell"""495 cursor_position = self.cursor_position496 SetConsoleCursorPosition(497 self._handle,498 coords=WindowsCoordinates(499 row=cursor_position.row - 1, col=cursor_position.col500 ),501 )502 503 def move_cursor_down(self) -> None:504 """Move the cursor down a single cell"""505 cursor_position = self.cursor_position506 SetConsoleCursorPosition(507 self._handle,508 coords=WindowsCoordinates(509 row=cursor_position.row + 1,510 col=cursor_position.col,511 ),512 )513 514 def move_cursor_forward(self) -> None:515 """Move the cursor forward a single cell. Wrap to the next line if required."""516 row, col = self.cursor_position517 if col == self.screen_size.col - 1:518 row += 1519 col = 0520 else:521 col += 1522 SetConsoleCursorPosition(523 self._handle, coords=WindowsCoordinates(row=row, col=col)524 )525 526 def move_cursor_to_column(self, column: int) -> None:527 """Move cursor to the column specified by the zero-based column index, staying on the same row528 529 Args:530 column (int): The zero-based column index to move the cursor to.531 """532 row, _ = self.cursor_position533 SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column))534 535 def move_cursor_backward(self) -> None:536 """Move the cursor backward a single cell. Wrap to the previous line if required."""537 row, col = self.cursor_position538 if col == 0:539 row -= 1540 col = self.screen_size.col - 1541 else:542 col -= 1543 SetConsoleCursorPosition(544 self._handle, coords=WindowsCoordinates(row=row, col=col)545 )546 547 def hide_cursor(self) -> None:548 """Hide the cursor"""549 current_cursor_size = self._get_cursor_size()550 invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=0)551 SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor)552 553 def show_cursor(self) -> None:554 """Show the cursor"""555 current_cursor_size = self._get_cursor_size()556 visible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=1)557 SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor)558 559 def set_title(self, title: str) -> None:560 """Set the title of the terminal window561 562 Args:563 title (str): The new title of the console window564 """565 assert len(title) < 255, "Console title must be less than 255 characters"566 SetConsoleTitle(title)567 568 def _get_cursor_size(self) -> int:569 """Get the percentage of the character cell that is filled by the cursor"""570 cursor_info = CONSOLE_CURSOR_INFO()571 GetConsoleCursorInfo(self._handle, cursor_info=cursor_info)572 return int(cursor_info.dwSize)573 574 575if __name__ == "__main__":576 handle = GetStdHandle()577 578 from rich.console import Console579 580 console = Console()581 582 term = LegacyWindowsTerm(sys.stdout)583 term.set_title("Win32 Console Examples")584 585 style = Style(color="black", bgcolor="red")586 587 heading = Style.parse("black on green")588 589 # Check colour output590 console.rule("Checking colour output")591 console.print("[on red]on red!")592 console.print("[blue]blue!")593 console.print("[yellow]yellow!")594 console.print("[bold yellow]bold yellow!")595 console.print("[bright_yellow]bright_yellow!")596 console.print("[dim bright_yellow]dim bright_yellow!")597 console.print("[italic cyan]italic cyan!")598 console.print("[bold white on blue]bold white on blue!")599 console.print("[reverse bold white on blue]reverse bold white on blue!")600 console.print("[bold black on cyan]bold black on cyan!")601 console.print("[black on green]black on green!")602 console.print("[blue on green]blue on green!")603 console.print("[white on black]white on black!")604 console.print("[black on white]black on white!")605 console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!")606 607 # Check cursor movement608 console.rule("Checking cursor movement")609 console.print()610 term.move_cursor_backward()611 term.move_cursor_backward()612 term.write_text("went back and wrapped to prev line")613 time.sleep(1)614 term.move_cursor_up()615 term.write_text("we go up")616 time.sleep(1)617 term.move_cursor_down()618 term.write_text("and down")619 time.sleep(1)620 term.move_cursor_up()621 term.move_cursor_backward()622 term.move_cursor_backward()623 term.write_text("we went up and back 2")624 time.sleep(1)625 term.move_cursor_down()626 term.move_cursor_backward()627 term.move_cursor_backward()628 term.write_text("we went down and back 2")629 time.sleep(1)630 631 # Check erasing of lines632 term.hide_cursor()633 console.print()634 console.rule("Checking line erasing")635 console.print("\n...Deleting to the start of the line...")636 term.write_text("The red arrow shows the cursor location, and direction of erase")637 time.sleep(1)638 term.move_cursor_to_column(16)639 term.write_styled("<", Style.parse("black on red"))640 term.move_cursor_backward()641 time.sleep(1)642 term.erase_start_of_line()643 time.sleep(1)644 645 console.print("\n\n...And to the end of the line...")646 term.write_text("The red arrow shows the cursor location, and direction of erase")647 time.sleep(1)648 649 term.move_cursor_to_column(16)650 term.write_styled(">", Style.parse("black on red"))651 time.sleep(1)652 term.erase_end_of_line()653 time.sleep(1)654 655 console.print("\n\n...Now the whole line will be erased...")656 term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan"))657 time.sleep(1)658 term.erase_line()659 660 term.show_cursor()661 print("\n")662 