win32.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. from __future__ import annotations
  2. import sys
  3. assert sys.platform == "win32"
  4. import os
  5. from ctypes import ArgumentError, byref, c_char, c_long, c_uint, c_ulong, pointer
  6. from ctypes.wintypes import DWORD, HANDLE
  7. from typing import Callable, TextIO, TypeVar
  8. from prompt_toolkit.cursor_shapes import CursorShape
  9. from prompt_toolkit.data_structures import Size
  10. from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs
  11. from prompt_toolkit.utils import get_cwidth
  12. from prompt_toolkit.win32_types import (
  13. CONSOLE_SCREEN_BUFFER_INFO,
  14. COORD,
  15. SMALL_RECT,
  16. STD_INPUT_HANDLE,
  17. STD_OUTPUT_HANDLE,
  18. )
  19. from ..utils import SPHINX_AUTODOC_RUNNING
  20. from .base import Output
  21. from .color_depth import ColorDepth
  22. # Do not import win32-specific stuff when generating documentation.
  23. # Otherwise RTD would be unable to generate docs for this module.
  24. if not SPHINX_AUTODOC_RUNNING:
  25. from ctypes import windll
  26. __all__ = [
  27. "Win32Output",
  28. ]
  29. def _coord_byval(coord: COORD) -> c_long:
  30. """
  31. Turns a COORD object into a c_long.
  32. This will cause it to be passed by value instead of by reference. (That is what I think at least.)
  33. When running ``ptipython`` is run (only with IPython), we often got the following error::
  34. Error in 'SetConsoleCursorPosition'.
  35. ArgumentError("argument 2: <class 'TypeError'>: wrong type",)
  36. argument 2: <class 'TypeError'>: wrong type
  37. It was solved by turning ``COORD`` parameters into a ``c_long`` like this.
  38. More info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx
  39. """
  40. return c_long(coord.Y * 0x10000 | coord.X & 0xFFFF)
  41. #: If True: write the output of the renderer also to the following file. This
  42. #: is very useful for debugging. (e.g.: to see that we don't write more bytes
  43. #: than required.)
  44. _DEBUG_RENDER_OUTPUT = False
  45. _DEBUG_RENDER_OUTPUT_FILENAME = r"prompt-toolkit-windows-output.log"
  46. class NoConsoleScreenBufferError(Exception):
  47. """
  48. Raised when the application is not running inside a Windows Console, but
  49. the user tries to instantiate Win32Output.
  50. """
  51. def __init__(self) -> None:
  52. # Are we running in 'xterm' on Windows, like git-bash for instance?
  53. xterm = "xterm" in os.environ.get("TERM", "")
  54. if xterm:
  55. message = (
  56. "Found {}, while expecting a Windows console. "
  57. 'Maybe try to run this program using "winpty" '
  58. "or run it in cmd.exe instead. Or otherwise, "
  59. "in case of Cygwin, use the Python executable "
  60. "that is compiled for Cygwin.".format(os.environ["TERM"])
  61. )
  62. else:
  63. message = "No Windows console found. Are you running cmd.exe?"
  64. super().__init__(message)
  65. _T = TypeVar("_T")
  66. class Win32Output(Output):
  67. """
  68. I/O abstraction for rendering to Windows consoles.
  69. (cmd.exe and similar.)
  70. """
  71. def __init__(
  72. self,
  73. stdout: TextIO,
  74. use_complete_width: bool = False,
  75. default_color_depth: ColorDepth | None = None,
  76. ) -> None:
  77. self.use_complete_width = use_complete_width
  78. self.default_color_depth = default_color_depth
  79. self._buffer: list[str] = []
  80. self.stdout: TextIO = stdout
  81. self.hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE))
  82. self._in_alternate_screen = False
  83. self._hidden = False
  84. self.color_lookup_table = ColorLookupTable()
  85. # Remember the default console colors.
  86. info = self.get_win32_screen_buffer_info()
  87. self.default_attrs = info.wAttributes if info else 15
  88. if _DEBUG_RENDER_OUTPUT:
  89. self.LOG = open(_DEBUG_RENDER_OUTPUT_FILENAME, "ab")
  90. def fileno(self) -> int:
  91. "Return file descriptor."
  92. return self.stdout.fileno()
  93. def encoding(self) -> str:
  94. "Return encoding used for stdout."
  95. return self.stdout.encoding
  96. def write(self, data: str) -> None:
  97. if self._hidden:
  98. data = " " * get_cwidth(data)
  99. self._buffer.append(data)
  100. def write_raw(self, data: str) -> None:
  101. "For win32, there is no difference between write and write_raw."
  102. self.write(data)
  103. def get_size(self) -> Size:
  104. info = self.get_win32_screen_buffer_info()
  105. # We take the width of the *visible* region as the size. Not the width
  106. # of the complete screen buffer. (Unless use_complete_width has been
  107. # set.)
  108. if self.use_complete_width:
  109. width = info.dwSize.X
  110. else:
  111. width = info.srWindow.Right - info.srWindow.Left
  112. height = info.srWindow.Bottom - info.srWindow.Top + 1
  113. # We avoid the right margin, windows will wrap otherwise.
  114. maxwidth = info.dwSize.X - 1
  115. width = min(maxwidth, width)
  116. # Create `Size` object.
  117. return Size(rows=height, columns=width)
  118. def _winapi(self, func: Callable[..., _T], *a: object, **kw: object) -> _T:
  119. """
  120. Flush and call win API function.
  121. """
  122. self.flush()
  123. if _DEBUG_RENDER_OUTPUT:
  124. self.LOG.write((f"{func.__name__!r}").encode() + b"\n")
  125. self.LOG.write(
  126. b" " + ", ".join([f"{i!r}" for i in a]).encode("utf-8") + b"\n"
  127. )
  128. self.LOG.write(
  129. b" "
  130. + ", ".join([f"{type(i)!r}" for i in a]).encode("utf-8")
  131. + b"\n"
  132. )
  133. self.LOG.flush()
  134. try:
  135. return func(*a, **kw)
  136. except ArgumentError as e:
  137. if _DEBUG_RENDER_OUTPUT:
  138. self.LOG.write((f" Error in {func.__name__!r} {e!r} {e}\n").encode())
  139. raise
  140. def get_win32_screen_buffer_info(self) -> CONSOLE_SCREEN_BUFFER_INFO:
  141. """
  142. Return Screen buffer info.
  143. """
  144. # NOTE: We don't call the `GetConsoleScreenBufferInfo` API through
  145. # `self._winapi`. Doing so causes Python to crash on certain 64bit
  146. # Python versions. (Reproduced with 64bit Python 2.7.6, on Windows
  147. # 10). It is not clear why. Possibly, it has to do with passing
  148. # these objects as an argument, or through *args.
  149. # The Python documentation contains the following - possibly related - warning:
  150. # ctypes does not support passing unions or structures with
  151. # bit-fields to functions by value. While this may work on 32-bit
  152. # x86, it's not guaranteed by the library to work in the general
  153. # case. Unions and structures with bit-fields should always be
  154. # passed to functions by pointer.
  155. # Also see:
  156. # - https://github.com/ipython/ipython/issues/10070
  157. # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/406
  158. # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/86
  159. self.flush()
  160. sbinfo = CONSOLE_SCREEN_BUFFER_INFO()
  161. success = windll.kernel32.GetConsoleScreenBufferInfo(
  162. self.hconsole, byref(sbinfo)
  163. )
  164. # success = self._winapi(windll.kernel32.GetConsoleScreenBufferInfo,
  165. # self.hconsole, byref(sbinfo))
  166. if success:
  167. return sbinfo
  168. else:
  169. raise NoConsoleScreenBufferError
  170. def set_title(self, title: str) -> None:
  171. """
  172. Set terminal title.
  173. """
  174. self._winapi(windll.kernel32.SetConsoleTitleW, title)
  175. def clear_title(self) -> None:
  176. self._winapi(windll.kernel32.SetConsoleTitleW, "")
  177. def erase_screen(self) -> None:
  178. start = COORD(0, 0)
  179. sbinfo = self.get_win32_screen_buffer_info()
  180. length = sbinfo.dwSize.X * sbinfo.dwSize.Y
  181. self.cursor_goto(row=0, column=0)
  182. self._erase(start, length)
  183. def erase_down(self) -> None:
  184. sbinfo = self.get_win32_screen_buffer_info()
  185. size = sbinfo.dwSize
  186. start = sbinfo.dwCursorPosition
  187. length = (size.X - size.X) + size.X * (size.Y - sbinfo.dwCursorPosition.Y)
  188. self._erase(start, length)
  189. def erase_end_of_line(self) -> None:
  190. """"""
  191. sbinfo = self.get_win32_screen_buffer_info()
  192. start = sbinfo.dwCursorPosition
  193. length = sbinfo.dwSize.X - sbinfo.dwCursorPosition.X
  194. self._erase(start, length)
  195. def _erase(self, start: COORD, length: int) -> None:
  196. chars_written = c_ulong()
  197. self._winapi(
  198. windll.kernel32.FillConsoleOutputCharacterA,
  199. self.hconsole,
  200. c_char(b" "),
  201. DWORD(length),
  202. _coord_byval(start),
  203. byref(chars_written),
  204. )
  205. # Reset attributes.
  206. sbinfo = self.get_win32_screen_buffer_info()
  207. self._winapi(
  208. windll.kernel32.FillConsoleOutputAttribute,
  209. self.hconsole,
  210. sbinfo.wAttributes,
  211. length,
  212. _coord_byval(start),
  213. byref(chars_written),
  214. )
  215. def reset_attributes(self) -> None:
  216. "Reset the console foreground/background color."
  217. self._winapi(
  218. windll.kernel32.SetConsoleTextAttribute, self.hconsole, self.default_attrs
  219. )
  220. self._hidden = False
  221. def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
  222. (
  223. fgcolor,
  224. bgcolor,
  225. bold,
  226. underline,
  227. strike,
  228. italic,
  229. blink,
  230. reverse,
  231. hidden,
  232. dim,
  233. ) = attrs
  234. self._hidden = bool(hidden)
  235. # Start from the default attributes.
  236. win_attrs: int = self.default_attrs
  237. if color_depth != ColorDepth.DEPTH_1_BIT:
  238. # Override the last four bits: foreground color.
  239. if fgcolor:
  240. win_attrs = win_attrs & ~0xF
  241. win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor)
  242. # Override the next four bits: background color.
  243. if bgcolor:
  244. win_attrs = win_attrs & ~0xF0
  245. win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor)
  246. # Reverse: swap these four bits groups.
  247. if reverse:
  248. win_attrs = (
  249. (win_attrs & ~0xFF)
  250. | ((win_attrs & 0xF) << 4)
  251. | ((win_attrs & 0xF0) >> 4)
  252. )
  253. self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs)
  254. def disable_autowrap(self) -> None:
  255. # Not supported by Windows.
  256. pass
  257. def enable_autowrap(self) -> None:
  258. # Not supported by Windows.
  259. pass
  260. def cursor_goto(self, row: int = 0, column: int = 0) -> None:
  261. pos = COORD(X=column, Y=row)
  262. self._winapi(
  263. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  264. )
  265. def cursor_up(self, amount: int) -> None:
  266. sr = self.get_win32_screen_buffer_info().dwCursorPosition
  267. pos = COORD(X=sr.X, Y=sr.Y - amount)
  268. self._winapi(
  269. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  270. )
  271. def cursor_down(self, amount: int) -> None:
  272. self.cursor_up(-amount)
  273. def cursor_forward(self, amount: int) -> None:
  274. sr = self.get_win32_screen_buffer_info().dwCursorPosition
  275. # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount)
  276. pos = COORD(X=max(0, sr.X + amount), Y=sr.Y)
  277. self._winapi(
  278. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  279. )
  280. def cursor_backward(self, amount: int) -> None:
  281. self.cursor_forward(-amount)
  282. def flush(self) -> None:
  283. """
  284. Write to output stream and flush.
  285. """
  286. if not self._buffer:
  287. # Only flush stdout buffer. (It could be that Python still has
  288. # something in its buffer. -- We want to be sure to print that in
  289. # the correct color.)
  290. self.stdout.flush()
  291. return
  292. data = "".join(self._buffer)
  293. if _DEBUG_RENDER_OUTPUT:
  294. self.LOG.write((f"{data!r}").encode() + b"\n")
  295. self.LOG.flush()
  296. # Print characters one by one. This appears to be the best solution
  297. # in order to avoid traces of vertical lines when the completion
  298. # menu disappears.
  299. for b in data:
  300. written = DWORD()
  301. retval = windll.kernel32.WriteConsoleW(
  302. self.hconsole, b, 1, byref(written), None
  303. )
  304. assert retval != 0
  305. self._buffer = []
  306. def get_rows_below_cursor_position(self) -> int:
  307. info = self.get_win32_screen_buffer_info()
  308. return info.srWindow.Bottom - info.dwCursorPosition.Y + 1
  309. def scroll_buffer_to_prompt(self) -> None:
  310. """
  311. To be called before drawing the prompt. This should scroll the console
  312. to left, with the cursor at the bottom (if possible).
  313. """
  314. # Get current window size
  315. info = self.get_win32_screen_buffer_info()
  316. sr = info.srWindow
  317. cursor_pos = info.dwCursorPosition
  318. result = SMALL_RECT()
  319. # Scroll to the left.
  320. result.Left = 0
  321. result.Right = sr.Right - sr.Left
  322. # Scroll vertical
  323. win_height = sr.Bottom - sr.Top
  324. if 0 < sr.Bottom - cursor_pos.Y < win_height - 1:
  325. # no vertical scroll if cursor already on the screen
  326. result.Bottom = sr.Bottom
  327. else:
  328. result.Bottom = max(win_height, cursor_pos.Y)
  329. result.Top = result.Bottom - win_height
  330. # Scroll API
  331. self._winapi(
  332. windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result)
  333. )
  334. def enter_alternate_screen(self) -> None:
  335. """
  336. Go to alternate screen buffer.
  337. """
  338. if not self._in_alternate_screen:
  339. GENERIC_READ = 0x80000000
  340. GENERIC_WRITE = 0x40000000
  341. # Create a new console buffer and activate that one.
  342. handle = HANDLE(
  343. self._winapi(
  344. windll.kernel32.CreateConsoleScreenBuffer,
  345. GENERIC_READ | GENERIC_WRITE,
  346. DWORD(0),
  347. None,
  348. DWORD(1),
  349. None,
  350. )
  351. )
  352. self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle)
  353. self.hconsole = handle
  354. self._in_alternate_screen = True
  355. def quit_alternate_screen(self) -> None:
  356. """
  357. Make stdout again the active buffer.
  358. """
  359. if self._in_alternate_screen:
  360. stdout = HANDLE(
  361. self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE)
  362. )
  363. self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout)
  364. self._winapi(windll.kernel32.CloseHandle, self.hconsole)
  365. self.hconsole = stdout
  366. self._in_alternate_screen = False
  367. def enable_mouse_support(self) -> None:
  368. ENABLE_MOUSE_INPUT = 0x10
  369. # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse
  370. # support to work, but it's possible that it was already cleared
  371. # before.
  372. ENABLE_QUICK_EDIT_MODE = 0x0040
  373. handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  374. original_mode = DWORD()
  375. self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode))
  376. self._winapi(
  377. windll.kernel32.SetConsoleMode,
  378. handle,
  379. (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE,
  380. )
  381. def disable_mouse_support(self) -> None:
  382. ENABLE_MOUSE_INPUT = 0x10
  383. handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  384. original_mode = DWORD()
  385. self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode))
  386. self._winapi(
  387. windll.kernel32.SetConsoleMode,
  388. handle,
  389. original_mode.value & ~ENABLE_MOUSE_INPUT,
  390. )
  391. def hide_cursor(self) -> None:
  392. pass
  393. def show_cursor(self) -> None:
  394. pass
  395. def set_cursor_shape(self, cursor_shape: CursorShape) -> None:
  396. pass
  397. def reset_cursor_shape(self) -> None:
  398. pass
  399. @classmethod
  400. def win32_refresh_window(cls) -> None:
  401. """
  402. Call win32 API to refresh the whole Window.
  403. This is sometimes necessary when the application paints background
  404. for completion menus. When the menu disappears, it leaves traces due
  405. to a bug in the Windows Console. Sending a repaint request solves it.
  406. """
  407. # Get console handle
  408. handle = HANDLE(windll.kernel32.GetConsoleWindow())
  409. RDW_INVALIDATE = 0x0001
  410. windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE))
  411. def get_default_color_depth(self) -> ColorDepth:
  412. """
  413. Return the default color depth for a windows terminal.
  414. Contrary to the Vt100 implementation, this doesn't depend on a $TERM
  415. variable.
  416. """
  417. if self.default_color_depth is not None:
  418. return self.default_color_depth
  419. return ColorDepth.DEPTH_4_BIT
  420. class FOREGROUND_COLOR:
  421. BLACK = 0x0000
  422. BLUE = 0x0001
  423. GREEN = 0x0002
  424. CYAN = 0x0003
  425. RED = 0x0004
  426. MAGENTA = 0x0005
  427. YELLOW = 0x0006
  428. GRAY = 0x0007
  429. INTENSITY = 0x0008 # Foreground color is intensified.
  430. class BACKGROUND_COLOR:
  431. BLACK = 0x0000
  432. BLUE = 0x0010
  433. GREEN = 0x0020
  434. CYAN = 0x0030
  435. RED = 0x0040
  436. MAGENTA = 0x0050
  437. YELLOW = 0x0060
  438. GRAY = 0x0070
  439. INTENSITY = 0x0080 # Background color is intensified.
  440. def _create_ansi_color_dict(
  441. color_cls: type[FOREGROUND_COLOR] | type[BACKGROUND_COLOR],
  442. ) -> dict[str, int]:
  443. "Create a table that maps the 16 named ansi colors to their Windows code."
  444. return {
  445. "ansidefault": color_cls.BLACK,
  446. "ansiblack": color_cls.BLACK,
  447. "ansigray": color_cls.GRAY,
  448. "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY,
  449. "ansiwhite": color_cls.GRAY | color_cls.INTENSITY,
  450. # Low intensity.
  451. "ansired": color_cls.RED,
  452. "ansigreen": color_cls.GREEN,
  453. "ansiyellow": color_cls.YELLOW,
  454. "ansiblue": color_cls.BLUE,
  455. "ansimagenta": color_cls.MAGENTA,
  456. "ansicyan": color_cls.CYAN,
  457. # High intensity.
  458. "ansibrightred": color_cls.RED | color_cls.INTENSITY,
  459. "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY,
  460. "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY,
  461. "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY,
  462. "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY,
  463. "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY,
  464. }
  465. FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR)
  466. BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR)
  467. assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  468. assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  469. class ColorLookupTable:
  470. """
  471. Inspired by pygments/formatters/terminal256.py
  472. """
  473. def __init__(self) -> None:
  474. self._win32_colors = self._build_color_table()
  475. # Cache (map color string to foreground and background code).
  476. self.best_match: dict[str, tuple[int, int]] = {}
  477. @staticmethod
  478. def _build_color_table() -> list[tuple[int, int, int, int, int]]:
  479. """
  480. Build an RGB-to-256 color conversion table
  481. """
  482. FG = FOREGROUND_COLOR
  483. BG = BACKGROUND_COLOR
  484. return [
  485. (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK),
  486. (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE),
  487. (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN),
  488. (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN),
  489. (0xAA, 0x00, 0x00, FG.RED, BG.RED),
  490. (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA),
  491. (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW),
  492. (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY),
  493. (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY),
  494. (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY),
  495. (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY),
  496. (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY),
  497. (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY),
  498. (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY),
  499. (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY),
  500. (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY),
  501. ]
  502. def _closest_color(self, r: int, g: int, b: int) -> tuple[int, int]:
  503. distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
  504. fg_match = 0
  505. bg_match = 0
  506. for r_, g_, b_, fg_, bg_ in self._win32_colors:
  507. rd = r - r_
  508. gd = g - g_
  509. bd = b - b_
  510. d = rd * rd + gd * gd + bd * bd
  511. if d < distance:
  512. fg_match = fg_
  513. bg_match = bg_
  514. distance = d
  515. return fg_match, bg_match
  516. def _color_indexes(self, color: str) -> tuple[int, int]:
  517. indexes = self.best_match.get(color, None)
  518. if indexes is None:
  519. try:
  520. rgb = int(str(color), 16)
  521. except ValueError:
  522. rgb = 0
  523. r = (rgb >> 16) & 0xFF
  524. g = (rgb >> 8) & 0xFF
  525. b = rgb & 0xFF
  526. indexes = self._closest_color(r, g, b)
  527. self.best_match[color] = indexes
  528. return indexes
  529. def lookup_fg_color(self, fg_color: str) -> int:
  530. """
  531. Return the color for use in the
  532. `windll.kernel32.SetConsoleTextAttribute` API call.
  533. :param fg_color: Foreground as text. E.g. 'ffffff' or 'red'
  534. """
  535. # Foreground.
  536. if fg_color in FG_ANSI_COLORS:
  537. return FG_ANSI_COLORS[fg_color]
  538. else:
  539. return self._color_indexes(fg_color)[0]
  540. def lookup_bg_color(self, bg_color: str) -> int:
  541. """
  542. Return the color for use in the
  543. `windll.kernel32.SetConsoleTextAttribute` API call.
  544. :param bg_color: Background as text. E.g. 'ffffff' or 'red'
  545. """
  546. # Background.
  547. if bg_color in BG_ANSI_COLORS:
  548. return BG_ANSI_COLORS[bg_color]
  549. else:
  550. return self._color_indexes(bg_color)[1]