123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- """Light wrapper around the Win32 Console API - this module should only be imported on Windows
- The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions
- """
- import ctypes
- import sys
- from typing import Any
- windll: Any = None
- if sys.platform == "win32":
- windll = ctypes.LibraryLoader(ctypes.WinDLL)
- else:
- raise ImportError(f"{__name__} can only be imported on Windows")
- import time
- from ctypes import Structure, byref, wintypes
- from typing import IO, NamedTuple, Type, cast
- from rich.color import ColorSystem
- from rich.style import Style
- STDOUT = -11
- ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
- COORD = wintypes._COORD
- class LegacyWindowsError(Exception):
- pass
- class WindowsCoordinates(NamedTuple):
- """Coordinates in the Windows Console API are (y, x), not (x, y).
- This class is intended to prevent that confusion.
- Rows and columns are indexed from 0.
- This class can be used in place of wintypes._COORD in arguments and argtypes.
- """
- row: int
- col: int
- @classmethod
- def from_param(cls, value: "WindowsCoordinates") -> COORD:
- """Converts a WindowsCoordinates into a wintypes _COORD structure.
- This classmethod is internally called by ctypes to perform the conversion.
- Args:
- value (WindowsCoordinates): The input coordinates to convert.
- Returns:
- wintypes._COORD: The converted coordinates struct.
- """
- return COORD(value.col, value.row)
- class CONSOLE_SCREEN_BUFFER_INFO(Structure):
- _fields_ = [
- ("dwSize", COORD),
- ("dwCursorPosition", COORD),
- ("wAttributes", wintypes.WORD),
- ("srWindow", wintypes.SMALL_RECT),
- ("dwMaximumWindowSize", COORD),
- ]
- class CONSOLE_CURSOR_INFO(ctypes.Structure):
- _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)]
- _GetStdHandle = windll.kernel32.GetStdHandle
- _GetStdHandle.argtypes = [
- wintypes.DWORD,
- ]
- _GetStdHandle.restype = wintypes.HANDLE
- def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE:
- """Retrieves a handle to the specified standard device (standard input, standard output, or standard error).
- Args:
- handle (int): Integer identifier for the handle. Defaults to -11 (stdout).
- Returns:
- wintypes.HANDLE: The handle
- """
- return cast(wintypes.HANDLE, _GetStdHandle(handle))
- _GetConsoleMode = windll.kernel32.GetConsoleMode
- _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]
- _GetConsoleMode.restype = wintypes.BOOL
- def GetConsoleMode(std_handle: wintypes.HANDLE) -> int:
- """Retrieves the current input mode of a console's input buffer
- or the current output mode of a console screen buffer.
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- Raises:
- LegacyWindowsError: If any error occurs while calling the Windows console API.
- Returns:
- int: Value representing the current console mode as documented at
- https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters
- """
- console_mode = wintypes.DWORD()
- success = bool(_GetConsoleMode(std_handle, console_mode))
- if not success:
- raise LegacyWindowsError("Unable to get legacy Windows Console Mode")
- return console_mode.value
- _FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW
- _FillConsoleOutputCharacterW.argtypes = [
- wintypes.HANDLE,
- ctypes.c_char,
- wintypes.DWORD,
- cast(Type[COORD], WindowsCoordinates),
- ctypes.POINTER(wintypes.DWORD),
- ]
- _FillConsoleOutputCharacterW.restype = wintypes.BOOL
- def FillConsoleOutputCharacter(
- std_handle: wintypes.HANDLE,
- char: str,
- length: int,
- start: WindowsCoordinates,
- ) -> int:
- """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- char (str): The character to write. Must be a string of length 1.
- length (int): The number of times to write the character.
- start (WindowsCoordinates): The coordinates to start writing at.
- Returns:
- int: The number of characters written.
- """
- character = ctypes.c_char(char.encode())
- num_characters = wintypes.DWORD(length)
- num_written = wintypes.DWORD(0)
- _FillConsoleOutputCharacterW(
- std_handle,
- character,
- num_characters,
- start,
- byref(num_written),
- )
- return num_written.value
- _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
- _FillConsoleOutputAttribute.argtypes = [
- wintypes.HANDLE,
- wintypes.WORD,
- wintypes.DWORD,
- cast(Type[COORD], WindowsCoordinates),
- ctypes.POINTER(wintypes.DWORD),
- ]
- _FillConsoleOutputAttribute.restype = wintypes.BOOL
- def FillConsoleOutputAttribute(
- std_handle: wintypes.HANDLE,
- attributes: int,
- length: int,
- start: WindowsCoordinates,
- ) -> int:
- """Sets the character attributes for a specified number of character cells,
- beginning at the specified coordinates in a screen buffer.
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- attributes (int): Integer value representing the foreground and background colours of the cells.
- length (int): The number of cells to set the output attribute of.
- start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set.
- Returns:
- int: The number of cells whose attributes were actually set.
- """
- num_cells = wintypes.DWORD(length)
- style_attrs = wintypes.WORD(attributes)
- num_written = wintypes.DWORD(0)
- _FillConsoleOutputAttribute(
- std_handle, style_attrs, num_cells, start, byref(num_written)
- )
- return num_written.value
- _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
- _SetConsoleTextAttribute.argtypes = [
- wintypes.HANDLE,
- wintypes.WORD,
- ]
- _SetConsoleTextAttribute.restype = wintypes.BOOL
- def SetConsoleTextAttribute(
- std_handle: wintypes.HANDLE, attributes: wintypes.WORD
- ) -> bool:
- """Set the colour attributes for all text written after this function is called.
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- attributes (int): Integer value representing the foreground and background colours.
- Returns:
- bool: True if the attribute was set successfully, otherwise False.
- """
- return bool(_SetConsoleTextAttribute(std_handle, attributes))
- _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
- _GetConsoleScreenBufferInfo.argtypes = [
- wintypes.HANDLE,
- ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
- ]
- _GetConsoleScreenBufferInfo.restype = wintypes.BOOL
- def GetConsoleScreenBufferInfo(
- std_handle: wintypes.HANDLE,
- ) -> CONSOLE_SCREEN_BUFFER_INFO:
- """Retrieves information about the specified console screen buffer.
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- Returns:
- CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about
- screen size, cursor position, colour attributes, and more."""
- console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO()
- _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info))
- return console_screen_buffer_info
- _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
- _SetConsoleCursorPosition.argtypes = [
- wintypes.HANDLE,
- cast(Type[COORD], WindowsCoordinates),
- ]
- _SetConsoleCursorPosition.restype = wintypes.BOOL
- def SetConsoleCursorPosition(
- std_handle: wintypes.HANDLE, coords: WindowsCoordinates
- ) -> bool:
- """Set the position of the cursor in the console screen
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- coords (WindowsCoordinates): The coordinates to move the cursor to.
- Returns:
- bool: True if the function succeeds, otherwise False.
- """
- return bool(_SetConsoleCursorPosition(std_handle, coords))
- _GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo
- _GetConsoleCursorInfo.argtypes = [
- wintypes.HANDLE,
- ctypes.POINTER(CONSOLE_CURSOR_INFO),
- ]
- _GetConsoleCursorInfo.restype = wintypes.BOOL
- def GetConsoleCursorInfo(
- std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
- ) -> bool:
- """Get the cursor info - used to get cursor visibility and width
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct that receives information
- about the console's cursor.
- Returns:
- bool: True if the function succeeds, otherwise False.
- """
- return bool(_GetConsoleCursorInfo(std_handle, byref(cursor_info)))
- _SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo
- _SetConsoleCursorInfo.argtypes = [
- wintypes.HANDLE,
- ctypes.POINTER(CONSOLE_CURSOR_INFO),
- ]
- _SetConsoleCursorInfo.restype = wintypes.BOOL
- def SetConsoleCursorInfo(
- std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
- ) -> bool:
- """Set the cursor info - used for adjusting cursor visibility and width
- Args:
- std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
- cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info.
- Returns:
- bool: True if the function succeeds, otherwise False.
- """
- return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info)))
- _SetConsoleTitle = windll.kernel32.SetConsoleTitleW
- _SetConsoleTitle.argtypes = [wintypes.LPCWSTR]
- _SetConsoleTitle.restype = wintypes.BOOL
- def SetConsoleTitle(title: str) -> bool:
- """Sets the title of the current console window
- Args:
- title (str): The new title of the console window.
- Returns:
- bool: True if the function succeeds, otherwise False.
- """
- return bool(_SetConsoleTitle(title))
- class LegacyWindowsTerm:
- """This class allows interaction with the legacy Windows Console API. It should only be used in the context
- of environments where virtual terminal processing is not available. However, if it is used in a Windows environment,
- the entire API should work.
- Args:
- file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout.
- """
- BRIGHT_BIT = 8
- # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers
- ANSI_TO_WINDOWS = [
- 0, # black The Windows colours are defined in wincon.h as follows:
- 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001
- 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010
- 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100
- 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000
- 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000
- 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000
- 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000
- 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000
- 12, # bright red
- 10, # bright green
- 14, # bright yellow
- 9, # bright blue
- 13, # bright magenta
- 11, # bright cyan
- 15, # bright white
- ]
- def __init__(self, file: "IO[str]") -> None:
- handle = GetStdHandle(STDOUT)
- self._handle = handle
- default_text = GetConsoleScreenBufferInfo(handle).wAttributes
- self._default_text = default_text
- self._default_fore = default_text & 7
- self._default_back = (default_text >> 4) & 7
- self._default_attrs = self._default_fore | (self._default_back << 4)
- self._file = file
- self.write = file.write
- self.flush = file.flush
- @property
- def cursor_position(self) -> WindowsCoordinates:
- """Returns the current position of the cursor (0-based)
- Returns:
- WindowsCoordinates: The current cursor position.
- """
- coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition
- return WindowsCoordinates(row=coord.Y, col=coord.X)
- @property
- def screen_size(self) -> WindowsCoordinates:
- """Returns the current size of the console screen buffer, in character columns and rows
- Returns:
- WindowsCoordinates: The width and height of the screen as WindowsCoordinates.
- """
- screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize
- return WindowsCoordinates(row=screen_size.Y, col=screen_size.X)
- def write_text(self, text: str) -> None:
- """Write text directly to the terminal without any modification of styles
- Args:
- text (str): The text to write to the console
- """
- self.write(text)
- self.flush()
- def write_styled(self, text: str, style: Style) -> None:
- """Write styled text to the terminal.
- Args:
- text (str): The text to write
- style (Style): The style of the text
- """
- color = style.color
- bgcolor = style.bgcolor
- if style.reverse:
- color, bgcolor = bgcolor, color
- if color:
- fore = color.downgrade(ColorSystem.WINDOWS).number
- fore = fore if fore is not None else 7 # Default to ANSI 7: White
- if style.bold:
- fore = fore | self.BRIGHT_BIT
- if style.dim:
- fore = fore & ~self.BRIGHT_BIT
- fore = self.ANSI_TO_WINDOWS[fore]
- else:
- fore = self._default_fore
- if bgcolor:
- back = bgcolor.downgrade(ColorSystem.WINDOWS).number
- back = back if back is not None else 0 # Default to ANSI 0: Black
- back = self.ANSI_TO_WINDOWS[back]
- else:
- back = self._default_back
- assert fore is not None
- assert back is not None
- SetConsoleTextAttribute(
- self._handle, attributes=ctypes.c_ushort(fore | (back << 4))
- )
- self.write_text(text)
- SetConsoleTextAttribute(self._handle, attributes=self._default_text)
- def move_cursor_to(self, new_position: WindowsCoordinates) -> None:
- """Set the position of the cursor
- Args:
- new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor.
- """
- if new_position.col < 0 or new_position.row < 0:
- return
- SetConsoleCursorPosition(self._handle, coords=new_position)
- def erase_line(self) -> None:
- """Erase all content on the line the cursor is currently located at"""
- screen_size = self.screen_size
- cursor_position = self.cursor_position
- cells_to_erase = screen_size.col
- start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0)
- FillConsoleOutputCharacter(
- self._handle, " ", length=cells_to_erase, start=start_coordinates
- )
- FillConsoleOutputAttribute(
- self._handle,
- self._default_attrs,
- length=cells_to_erase,
- start=start_coordinates,
- )
- def erase_end_of_line(self) -> None:
- """Erase all content from the cursor position to the end of that line"""
- cursor_position = self.cursor_position
- cells_to_erase = self.screen_size.col - cursor_position.col
- FillConsoleOutputCharacter(
- self._handle, " ", length=cells_to_erase, start=cursor_position
- )
- FillConsoleOutputAttribute(
- self._handle,
- self._default_attrs,
- length=cells_to_erase,
- start=cursor_position,
- )
- def erase_start_of_line(self) -> None:
- """Erase all content from the cursor position to the start of that line"""
- row, col = self.cursor_position
- start = WindowsCoordinates(row, 0)
- FillConsoleOutputCharacter(self._handle, " ", length=col, start=start)
- FillConsoleOutputAttribute(
- self._handle, self._default_attrs, length=col, start=start
- )
- def move_cursor_up(self) -> None:
- """Move the cursor up a single cell"""
- cursor_position = self.cursor_position
- SetConsoleCursorPosition(
- self._handle,
- coords=WindowsCoordinates(
- row=cursor_position.row - 1, col=cursor_position.col
- ),
- )
- def move_cursor_down(self) -> None:
- """Move the cursor down a single cell"""
- cursor_position = self.cursor_position
- SetConsoleCursorPosition(
- self._handle,
- coords=WindowsCoordinates(
- row=cursor_position.row + 1,
- col=cursor_position.col,
- ),
- )
- def move_cursor_forward(self) -> None:
- """Move the cursor forward a single cell. Wrap to the next line if required."""
- row, col = self.cursor_position
- if col == self.screen_size.col - 1:
- row += 1
- col = 0
- else:
- col += 1
- SetConsoleCursorPosition(
- self._handle, coords=WindowsCoordinates(row=row, col=col)
- )
- def move_cursor_to_column(self, column: int) -> None:
- """Move cursor to the column specified by the zero-based column index, staying on the same row
- Args:
- column (int): The zero-based column index to move the cursor to.
- """
- row, _ = self.cursor_position
- SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column))
- def move_cursor_backward(self) -> None:
- """Move the cursor backward a single cell. Wrap to the previous line if required."""
- row, col = self.cursor_position
- if col == 0:
- row -= 1
- col = self.screen_size.col - 1
- else:
- col -= 1
- SetConsoleCursorPosition(
- self._handle, coords=WindowsCoordinates(row=row, col=col)
- )
- def hide_cursor(self) -> None:
- """Hide the cursor"""
- current_cursor_size = self._get_cursor_size()
- invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=0)
- SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor)
- def show_cursor(self) -> None:
- """Show the cursor"""
- current_cursor_size = self._get_cursor_size()
- visible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=1)
- SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor)
- def set_title(self, title: str) -> None:
- """Set the title of the terminal window
- Args:
- title (str): The new title of the console window
- """
- assert len(title) < 255, "Console title must be less than 255 characters"
- SetConsoleTitle(title)
- def _get_cursor_size(self) -> int:
- """Get the percentage of the character cell that is filled by the cursor"""
- cursor_info = CONSOLE_CURSOR_INFO()
- GetConsoleCursorInfo(self._handle, cursor_info=cursor_info)
- return int(cursor_info.dwSize)
- if __name__ == "__main__":
- handle = GetStdHandle()
- from rich.console import Console
- console = Console()
- term = LegacyWindowsTerm(sys.stdout)
- term.set_title("Win32 Console Examples")
- style = Style(color="black", bgcolor="red")
- heading = Style.parse("black on green")
- # Check colour output
- console.rule("Checking colour output")
- console.print("[on red]on red!")
- console.print("[blue]blue!")
- console.print("[yellow]yellow!")
- console.print("[bold yellow]bold yellow!")
- console.print("[bright_yellow]bright_yellow!")
- console.print("[dim bright_yellow]dim bright_yellow!")
- console.print("[italic cyan]italic cyan!")
- console.print("[bold white on blue]bold white on blue!")
- console.print("[reverse bold white on blue]reverse bold white on blue!")
- console.print("[bold black on cyan]bold black on cyan!")
- console.print("[black on green]black on green!")
- console.print("[blue on green]blue on green!")
- console.print("[white on black]white on black!")
- console.print("[black on white]black on white!")
- console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!")
- # Check cursor movement
- console.rule("Checking cursor movement")
- console.print()
- term.move_cursor_backward()
- term.move_cursor_backward()
- term.write_text("went back and wrapped to prev line")
- time.sleep(1)
- term.move_cursor_up()
- term.write_text("we go up")
- time.sleep(1)
- term.move_cursor_down()
- term.write_text("and down")
- time.sleep(1)
- term.move_cursor_up()
- term.move_cursor_backward()
- term.move_cursor_backward()
- term.write_text("we went up and back 2")
- time.sleep(1)
- term.move_cursor_down()
- term.move_cursor_backward()
- term.move_cursor_backward()
- term.write_text("we went down and back 2")
- time.sleep(1)
- # Check erasing of lines
- term.hide_cursor()
- console.print()
- console.rule("Checking line erasing")
- console.print("\n...Deleting to the start of the line...")
- term.write_text("The red arrow shows the cursor location, and direction of erase")
- time.sleep(1)
- term.move_cursor_to_column(16)
- term.write_styled("<", Style.parse("black on red"))
- term.move_cursor_backward()
- time.sleep(1)
- term.erase_start_of_line()
- time.sleep(1)
- console.print("\n\n...And to the end of the line...")
- term.write_text("The red arrow shows the cursor location, and direction of erase")
- time.sleep(1)
- term.move_cursor_to_column(16)
- term.write_styled(">", Style.parse("black on red"))
- time.sleep(1)
- term.erase_end_of_line()
- time.sleep(1)
- console.print("\n\n...Now the whole line will be erased...")
- term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan"))
- time.sleep(1)
- term.erase_line()
- term.show_cursor()
- print("\n")
|