from __future__ import annotations
import io
import os
import sys
import ctypes
import types
from ctypes.wintypes import (
_COORD,
WORD,
SMALL_RECT,
BOOL,
HANDLE,
CHAR,
DWORD,
WCHAR,
SHORT,
)
from ctypes import Structure, POINTER, Union
from .console import Event, Console
from .trace import trace
from .utils import wlen
from .windows_eventqueue import EventQueue
try:
from ctypes import get_last_error, GetLastError, WinDLL, windll, WinError except:
from ctypes import CDLL as WinDLL, cdll as windll
def GetLastError() -> int:
return 42
def get_last_error() -> int:
return 42
class WinError(OSError): def __init__(self, err: int | None, descr: str | None = None) -> None:
self.err = err
self.descr = descr
nt: types.ModuleType | None
try:
import nt
except ImportError:
nt = None
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO
VK_MAP: dict[int, str] = {
0x23: "end", 0x24: "home", 0x25: "left", 0x26: "up", 0x27: "right", 0x28: "down", 0x2E: "delete", 0x70: "f1", 0x71: "f2", 0x72: "f3", 0x73: "f4", 0x74: "f5", 0x75: "f6", 0x76: "f7", 0x77: "f8", 0x78: "f9", 0x79: "f10", 0x7A: "f11", 0x7B: "f12", 0x7C: "f13", 0x7D: "f14", 0x7E: "f15", 0x7F: "f16", 0x80: "f17", 0x81: "f18", 0x82: "f19", 0x83: "f20", }
ERASE_IN_LINE = "\x1b[K"
MOVE_LEFT = "\x1b[{}D"
MOVE_RIGHT = "\x1b[{}C"
MOVE_UP = "\x1b[{}A"
MOVE_DOWN = "\x1b[{}B"
CLEAR = "\x1b[H\x1b[J"
ALT_ACTIVE = 0x01 | 0x02
CTRL_ACTIVE = 0x04 | 0x08
WAIT_TIMEOUT = 0x102
WAIT_FAILED = 0xFFFFFFFF
INFINITE = 0xFFFFFFFF
class _error(Exception):
pass
def _supports_vt():
try:
return nt._supports_virtual_terminal()
except AttributeError:
return False
class WindowsConsole(Console):
def __init__(
self,
f_in: IO[bytes] | int = 0,
f_out: IO[bytes] | int = 1,
term: str = "",
encoding: str = "",
):
super().__init__(f_in, f_out, term, encoding)
self.__vt_support = _supports_vt()
if self.__vt_support:
trace('console supports virtual terminal')
original_input_mode = DWORD()
GetConsoleMode(InHandle, original_input_mode)
trace(f'saved original input mode 0x{original_input_mode.value:x}')
self.__original_input_mode = original_input_mode.value
SetConsoleMode(
OutHandle,
ENABLE_WRAP_AT_EOL_OUTPUT
| ENABLE_PROCESSED_OUTPUT
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)
self.screen: list[str] = []
self.width = 80
self.height = 25
self.__offset = 0
self.event_queue = EventQueue(encoding)
try:
self.out = io._WindowsConsoleIO(self.output_fd, "w") except ValueError:
self.out = None
def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
cx, cy = c_xy
while len(self.screen) < min(len(screen), self.height):
self._hide_cursor()
if self.screen:
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")
px, py = self.posxy
old_offset = offset = self.__offset
height = self.height
if cy < offset:
offset = cy
elif cy >= offset + height:
offset = cy - height + 1
scroll_lines = offset - old_offset
self._scroll(scroll_lines, self._getscrollbacksize())
self.posxy = self.posxy[0], self.posxy[1] + scroll_lines
self.__offset += scroll_lines
for i in range(scroll_lines):
self.screen.append("")
elif offset > 0 and len(screen) < offset + height:
offset = max(len(screen) - height, 0)
screen.append("")
oldscr = self.screen[old_offset : old_offset + height]
newscr = screen[offset : offset + height]
self.__offset = offset
self._hide_cursor()
for (
y,
oldline,
newline,
) in zip(range(offset, offset + height), oldscr, newscr):
if oldline != newline:
self.__write_changed_line(y, oldline, newline, px)
y = len(newscr)
while y < len(oldscr):
self._move_relative(0, y)
self.posxy = 0, y
self._erase_to_end()
y += 1
self._show_cursor()
self.screen = screen
self.move_cursor(cx, cy)
@property
def input_hook(self):
if nt is not None and nt._is_inputhook_installed():
return nt._inputhook
def __write_changed_line(
self, y: int, oldline: str, newline: str, px_coord: int
) -> None:
minlen = min(wlen(oldline), wlen(newline))
x_pos = 0
x_coord = 0
px_pos = 0
j = 0
for c in oldline:
if j >= px_coord:
break
j += wlen(c)
px_pos += 1
while (
x_coord < minlen
and oldline[x_pos] == newline[x_pos]
and newline[x_pos] != "\x1b"
):
x_coord += wlen(newline[x_pos])
x_pos += 1
self._hide_cursor()
self._move_relative(x_coord, y)
if wlen(oldline) > wlen(newline):
self._erase_to_end()
self.__write(newline[x_pos:])
self.posxy = min(wlen(newline), self.width - 1), y
if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
self.move_cursor(0, y)
def _scroll(
self, top: int, bottom: int, left: int | None = None, right: int | None = None
) -> None:
scroll_rect = SMALL_RECT()
scroll_rect.Top = SHORT(top)
scroll_rect.Bottom = SHORT(bottom)
scroll_rect.Left = SHORT(0 if left is None else left)
scroll_rect.Right = SHORT(
self.getheightwidth()[1] - 1 if right is None else right
)
destination_origin = _COORD()
fill_info = CHAR_INFO()
fill_info.UnicodeChar = " "
if not ScrollConsoleScreenBuffer(
OutHandle, scroll_rect, None, destination_origin, fill_info
):
raise WinError(GetLastError())
def _hide_cursor(self):
self.__write("\x1b[?25l")
def _show_cursor(self):
self.__write("\x1b[?25h")
def _enable_blinking(self):
self.__write("\x1b[?12h")
def _disable_blinking(self):
self.__write("\x1b[?12l")
def _enable_bracketed_paste(self) -> None:
self.__write("\x1b[?2004h")
def _disable_bracketed_paste(self) -> None:
self.__write("\x1b[?2004l")
def __write(self, text: str) -> None:
if "\x1a" in text:
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
if self.out is not None:
self.out.write(text.encode(self.encoding, "replace"))
self.out.flush()
else:
os.write(self.output_fd, text.encode(self.encoding, "replace"))
@property
def screen_xy(self) -> tuple[int, int]:
info = CONSOLE_SCREEN_BUFFER_INFO()
if not GetConsoleScreenBufferInfo(OutHandle, info):
raise WinError(GetLastError())
return info.dwCursorPosition.X, info.dwCursorPosition.Y
def _erase_to_end(self) -> None:
self.__write(ERASE_IN_LINE)
def prepare(self) -> None:
trace("prepare")
self.screen = []
self.height, self.width = self.getheightwidth()
self.posxy = 0, 0
self.__gone_tall = 0
self.__offset = 0
if self.__vt_support:
SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT)
self._enable_bracketed_paste()
def restore(self) -> None:
if self.__vt_support:
self._disable_bracketed_paste()
SetConsoleMode(InHandle, self.__original_input_mode)
def _move_relative(self, x: int, y: int) -> None:
dx = x - self.posxy[0]
dy = y - self.posxy[1]
if dx < 0:
self.__write(MOVE_LEFT.format(-dx))
elif dx > 0:
self.__write(MOVE_RIGHT.format(dx))
if dy < 0:
self.__write(MOVE_UP.format(-dy))
elif dy > 0:
self.__write(MOVE_DOWN.format(dy))
def move_cursor(self, x: int, y: int) -> None:
if x < 0 or y < 0:
raise ValueError(f"Bad cursor position {x}, {y}")
if y < self.__offset or y >= self.__offset + self.height:
self.event_queue.insert(Event("scroll", ""))
else:
self._move_relative(x, y)
self.posxy = x, y
def set_cursor_vis(self, visible: bool) -> None:
if visible:
self._show_cursor()
else:
self._hide_cursor()
def getheightwidth(self) -> tuple[int, int]:
info = CONSOLE_SCREEN_BUFFER_INFO()
if not GetConsoleScreenBufferInfo(OutHandle, info):
raise WinError(GetLastError())
return (
info.srWindow.Bottom - info.srWindow.Top + 1,
info.srWindow.Right - info.srWindow.Left + 1,
)
def _getscrollbacksize(self) -> int:
info = CONSOLE_SCREEN_BUFFER_INFO()
if not GetConsoleScreenBufferInfo(OutHandle, info):
raise WinError(GetLastError())
return info.srWindow.Bottom
def _read_input(self) -> INPUT_RECORD | None:
rec = INPUT_RECORD()
read = DWORD()
if not ReadConsoleInput(InHandle, rec, 1, read):
raise WinError(GetLastError())
return rec
def _read_input_bulk(
self, n: int
) -> tuple[ctypes.Array[INPUT_RECORD], int]:
rec = (n * INPUT_RECORD)()
read = DWORD()
if not ReadConsoleInput(InHandle, rec, n, read):
raise WinError(GetLastError())
return rec, read.value
def get_event(self, block: bool = True) -> Event | None:
if not block and not self.wait(timeout=0):
return None
while self.event_queue.empty():
rec = self._read_input()
if rec is None:
return None
if rec.EventType == WINDOW_BUFFER_SIZE_EVENT:
return Event("resize", "")
if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown:
if block:
continue
return None
key_event = rec.Event.KeyEvent
raw_key = key = key_event.uChar.UnicodeChar
if key == "\r":
return Event(evt="key", data="\n")
elif key_event.wVirtualKeyCode == 8:
key = "backspace"
elif key == "\x00":
key = VK_MAP.get(key_event.wVirtualKeyCode)
if key:
if key_event.dwControlKeyState & CTRL_ACTIVE:
key = f"ctrl {key}"
elif key_event.dwControlKeyState & ALT_ACTIVE:
self.event_queue.insert(Event(evt="key", data=key))
return Event(evt="key", data="\033") return Event(evt="key", data=key)
if block:
continue
return None
elif self.__vt_support:
for char in raw_key.encode(self.event_queue.encoding, "replace"):
self.event_queue.push(char)
continue
if key_event.dwControlKeyState & ALT_ACTIVE:
if not key_event.dwControlKeyState & CTRL_ACTIVE:
self.event_queue.insert(Event(evt="key", data=key))
return Event(evt="key", data="\033")
return Event(evt="key", data=key)
return self.event_queue.get()
def push_char(self, char: int | bytes) -> None:
raise NotImplementedError("push_char not supported on Windows")
def beep(self) -> None:
self.__write("\x07")
def clear(self) -> None:
self.__write(CLEAR)
self.posxy = 0, 0
self.screen = []
def finish(self) -> None:
y = len(self.screen) - 1
while y >= 0 and not self.screen[y]:
y -= 1
self._move_relative(0, min(y, self.height + self.__offset - 1))
self.__write("\r\n")
def flushoutput(self) -> None:
pass
def forgetinput(self) -> None:
if not FlushConsoleInputBuffer(InHandle):
raise WinError(GetLastError())
def getpending(self) -> Event:
e = Event("key", "", b"")
while not self.event_queue.empty():
e2 = self.event_queue.get()
if e2:
e.data += e2.data
recs, rec_count = self._read_input_bulk(1024)
for i in range(rec_count):
rec = recs[i]
if rec and rec.EventType == KEY_EVENT:
key_event = rec.Event.KeyEvent
if not key_event.bKeyDown:
continue
ch = key_event.uChar.UnicodeChar
if ch == "\x00":
continue
if ch == "\r":
ch += "\n"
e.data += ch
return e
def wait_for_event(self, timeout: float | None) -> bool:
if timeout is None:
timeout = INFINITE
else:
timeout = int(timeout)
ret = WaitForSingleObject(InHandle, timeout)
if ret == WAIT_FAILED:
raise WinError(get_last_error())
elif ret == WAIT_TIMEOUT:
return False
return True
def wait(self, timeout: float | None) -> bool:
return (
not self.event_queue.empty()
or self.wait_for_event(timeout)
)
def repaint(self) -> None:
raise NotImplementedError("No repaint support")
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
_fields_ = [
("dwSize", _COORD),
("dwCursorPosition", _COORD),
("wAttributes", WORD),
("srWindow", SMALL_RECT),
("dwMaximumWindowSize", _COORD),
]
class CONSOLE_CURSOR_INFO(Structure):
_fields_ = [
("dwSize", DWORD),
("bVisible", BOOL),
]
class CHAR_INFO(Structure):
_fields_ = [
("UnicodeChar", WCHAR),
("Attributes", WORD),
]
class Char(Union):
_fields_ = [
("UnicodeChar", WCHAR),
("Char", CHAR),
]
class KeyEvent(ctypes.Structure):
_fields_ = [
("bKeyDown", BOOL),
("wRepeatCount", WORD),
("wVirtualKeyCode", WORD),
("wVirtualScanCode", WORD),
("uChar", Char),
("dwControlKeyState", DWORD),
]
class WindowsBufferSizeEvent(ctypes.Structure):
_fields_ = [("dwSize", _COORD)]
class ConsoleEvent(ctypes.Union):
_fields_ = [
("KeyEvent", KeyEvent),
("WindowsBufferSizeEvent", WindowsBufferSizeEvent),
]
class INPUT_RECORD(Structure):
_fields_ = [("EventType", WORD), ("Event", ConsoleEvent)]
KEY_EVENT = 0x01
FOCUS_EVENT = 0x10
MENU_EVENT = 0x08
MOUSE_EVENT = 0x02
WINDOW_BUFFER_SIZE_EVENT = 0x04
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
ENABLE_PROCESSED_OUTPUT = 0x01
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
if sys.platform == "win32":
_KERNEL32 = WinDLL("kernel32", use_last_error=True)
GetStdHandle = windll.kernel32.GetStdHandle
GetStdHandle.argtypes = [DWORD]
GetStdHandle.restype = HANDLE
GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo
GetConsoleScreenBufferInfo.argtypes = [
HANDLE,
ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
]
GetConsoleScreenBufferInfo.restype = BOOL
ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW
ScrollConsoleScreenBuffer.argtypes = [
HANDLE,
POINTER(SMALL_RECT),
POINTER(SMALL_RECT),
_COORD,
POINTER(CHAR_INFO),
]
ScrollConsoleScreenBuffer.restype = BOOL
GetConsoleMode = _KERNEL32.GetConsoleMode
GetConsoleMode.argtypes = [HANDLE, POINTER(DWORD)]
GetConsoleMode.restype = BOOL
SetConsoleMode = _KERNEL32.SetConsoleMode
SetConsoleMode.argtypes = [HANDLE, DWORD]
SetConsoleMode.restype = BOOL
ReadConsoleInput = _KERNEL32.ReadConsoleInputW
ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)]
ReadConsoleInput.restype = BOOL
FlushConsoleInputBuffer = _KERNEL32.FlushConsoleInputBuffer
FlushConsoleInputBuffer.argtypes = [HANDLE]
FlushConsoleInputBuffer.restype = BOOL
WaitForSingleObject = _KERNEL32.WaitForSingleObject
WaitForSingleObject.argtypes = [HANDLE, DWORD]
WaitForSingleObject.restype = DWORD
OutHandle = GetStdHandle(STD_OUTPUT_HANDLE)
InHandle = GetStdHandle(STD_INPUT_HANDLE)
else:
def _win_only(*args, **kwargs):
raise NotImplementedError("Windows only")
GetStdHandle = _win_only
GetConsoleScreenBufferInfo = _win_only
ScrollConsoleScreenBuffer = _win_only
GetConsoleMode = _win_only
SetConsoleMode = _win_only
ReadConsoleInput = _win_only
FlushConsoleInputBuffer = _win_only
WaitForSingleObject = _win_only
OutHandle = 0
InHandle = 0