win32.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  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 %s, 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." % 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(("%r" % func.__name__).encode("utf-8") + b"\n")
  125. self.LOG.write(
  126. b" " + ", ".join(["%r" % i for i in a]).encode("utf-8") + b"\n"
  127. )
  128. self.LOG.write(
  129. b" "
  130. + ", ".join(["%r" % type(i) 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. ) = attrs
  233. self._hidden = bool(hidden)
  234. # Start from the default attributes.
  235. win_attrs: int = self.default_attrs
  236. if color_depth != ColorDepth.DEPTH_1_BIT:
  237. # Override the last four bits: foreground color.
  238. if fgcolor:
  239. win_attrs = win_attrs & ~0xF
  240. win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor)
  241. # Override the next four bits: background color.
  242. if bgcolor:
  243. win_attrs = win_attrs & ~0xF0
  244. win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor)
  245. # Reverse: swap these four bits groups.
  246. if reverse:
  247. win_attrs = (
  248. (win_attrs & ~0xFF)
  249. | ((win_attrs & 0xF) << 4)
  250. | ((win_attrs & 0xF0) >> 4)
  251. )
  252. self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs)
  253. def disable_autowrap(self) -> None:
  254. # Not supported by Windows.
  255. pass
  256. def enable_autowrap(self) -> None:
  257. # Not supported by Windows.
  258. pass
  259. def cursor_goto(self, row: int = 0, column: int = 0) -> None:
  260. pos = COORD(X=column, Y=row)
  261. self._winapi(
  262. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  263. )
  264. def cursor_up(self, amount: int) -> None:
  265. sr = self.get_win32_screen_buffer_info().dwCursorPosition
  266. pos = COORD(X=sr.X, Y=sr.Y - amount)
  267. self._winapi(
  268. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  269. )
  270. def cursor_down(self, amount: int) -> None:
  271. self.cursor_up(-amount)
  272. def cursor_forward(self, amount: int) -> None:
  273. sr = self.get_win32_screen_buffer_info().dwCursorPosition
  274. # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount)
  275. pos = COORD(X=max(0, sr.X + amount), Y=sr.Y)
  276. self._winapi(
  277. windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos)
  278. )
  279. def cursor_backward(self, amount: int) -> None:
  280. self.cursor_forward(-amount)
  281. def flush(self) -> None:
  282. """
  283. Write to output stream and flush.
  284. """
  285. if not self._buffer:
  286. # Only flush stdout buffer. (It could be that Python still has
  287. # something in its buffer. -- We want to be sure to print that in
  288. # the correct color.)
  289. self.stdout.flush()
  290. return
  291. data = "".join(self._buffer)
  292. if _DEBUG_RENDER_OUTPUT:
  293. self.LOG.write(("%r" % data).encode("utf-8") + b"\n")
  294. self.LOG.flush()
  295. # Print characters one by one. This appears to be the best solution
  296. # in order to avoid traces of vertical lines when the completion
  297. # menu disappears.
  298. for b in data:
  299. written = DWORD()
  300. retval = windll.kernel32.WriteConsoleW(
  301. self.hconsole, b, 1, byref(written), None
  302. )
  303. assert retval != 0
  304. self._buffer = []
  305. def get_rows_below_cursor_position(self) -> int:
  306. info = self.get_win32_screen_buffer_info()
  307. return info.srWindow.Bottom - info.dwCursorPosition.Y + 1
  308. def scroll_buffer_to_prompt(self) -> None:
  309. """
  310. To be called before drawing the prompt. This should scroll the console
  311. to left, with the cursor at the bottom (if possible).
  312. """
  313. # Get current window size
  314. info = self.get_win32_screen_buffer_info()
  315. sr = info.srWindow
  316. cursor_pos = info.dwCursorPosition
  317. result = SMALL_RECT()
  318. # Scroll to the left.
  319. result.Left = 0
  320. result.Right = sr.Right - sr.Left
  321. # Scroll vertical
  322. win_height = sr.Bottom - sr.Top
  323. if 0 < sr.Bottom - cursor_pos.Y < win_height - 1:
  324. # no vertical scroll if cursor already on the screen
  325. result.Bottom = sr.Bottom
  326. else:
  327. result.Bottom = max(win_height, cursor_pos.Y)
  328. result.Top = result.Bottom - win_height
  329. # Scroll API
  330. self._winapi(
  331. windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result)
  332. )
  333. def enter_alternate_screen(self) -> None:
  334. """
  335. Go to alternate screen buffer.
  336. """
  337. if not self._in_alternate_screen:
  338. GENERIC_READ = 0x80000000
  339. GENERIC_WRITE = 0x40000000
  340. # Create a new console buffer and activate that one.
  341. handle = HANDLE(
  342. self._winapi(
  343. windll.kernel32.CreateConsoleScreenBuffer,
  344. GENERIC_READ | GENERIC_WRITE,
  345. DWORD(0),
  346. None,
  347. DWORD(1),
  348. None,
  349. )
  350. )
  351. self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle)
  352. self.hconsole = handle
  353. self._in_alternate_screen = True
  354. def quit_alternate_screen(self) -> None:
  355. """
  356. Make stdout again the active buffer.
  357. """
  358. if self._in_alternate_screen:
  359. stdout = HANDLE(
  360. self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE)
  361. )
  362. self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout)
  363. self._winapi(windll.kernel32.CloseHandle, self.hconsole)
  364. self.hconsole = stdout
  365. self._in_alternate_screen = False
  366. def enable_mouse_support(self) -> None:
  367. ENABLE_MOUSE_INPUT = 0x10
  368. # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse
  369. # support to work, but it's possible that it was already cleared
  370. # before.
  371. ENABLE_QUICK_EDIT_MODE = 0x0040
  372. handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  373. original_mode = DWORD()
  374. self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode))
  375. self._winapi(
  376. windll.kernel32.SetConsoleMode,
  377. handle,
  378. (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE,
  379. )
  380. def disable_mouse_support(self) -> None:
  381. ENABLE_MOUSE_INPUT = 0x10
  382. handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  383. original_mode = DWORD()
  384. self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode))
  385. self._winapi(
  386. windll.kernel32.SetConsoleMode,
  387. handle,
  388. original_mode.value & ~ENABLE_MOUSE_INPUT,
  389. )
  390. def hide_cursor(self) -> None:
  391. pass
  392. def show_cursor(self) -> None:
  393. pass
  394. def set_cursor_shape(self, cursor_shape: CursorShape) -> None:
  395. pass
  396. def reset_cursor_shape(self) -> None:
  397. pass
  398. @classmethod
  399. def win32_refresh_window(cls) -> None:
  400. """
  401. Call win32 API to refresh the whole Window.
  402. This is sometimes necessary when the application paints background
  403. for completion menus. When the menu disappears, it leaves traces due
  404. to a bug in the Windows Console. Sending a repaint request solves it.
  405. """
  406. # Get console handle
  407. handle = HANDLE(windll.kernel32.GetConsoleWindow())
  408. RDW_INVALIDATE = 0x0001
  409. windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE))
  410. def get_default_color_depth(self) -> ColorDepth:
  411. """
  412. Return the default color depth for a windows terminal.
  413. Contrary to the Vt100 implementation, this doesn't depend on a $TERM
  414. variable.
  415. """
  416. if self.default_color_depth is not None:
  417. return self.default_color_depth
  418. return ColorDepth.DEPTH_4_BIT
  419. class FOREGROUND_COLOR:
  420. BLACK = 0x0000
  421. BLUE = 0x0001
  422. GREEN = 0x0002
  423. CYAN = 0x0003
  424. RED = 0x0004
  425. MAGENTA = 0x0005
  426. YELLOW = 0x0006
  427. GRAY = 0x0007
  428. INTENSITY = 0x0008 # Foreground color is intensified.
  429. class BACKGROUND_COLOR:
  430. BLACK = 0x0000
  431. BLUE = 0x0010
  432. GREEN = 0x0020
  433. CYAN = 0x0030
  434. RED = 0x0040
  435. MAGENTA = 0x0050
  436. YELLOW = 0x0060
  437. GRAY = 0x0070
  438. INTENSITY = 0x0080 # Background color is intensified.
  439. def _create_ansi_color_dict(
  440. color_cls: type[FOREGROUND_COLOR] | type[BACKGROUND_COLOR],
  441. ) -> dict[str, int]:
  442. "Create a table that maps the 16 named ansi colors to their Windows code."
  443. return {
  444. "ansidefault": color_cls.BLACK,
  445. "ansiblack": color_cls.BLACK,
  446. "ansigray": color_cls.GRAY,
  447. "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY,
  448. "ansiwhite": color_cls.GRAY | color_cls.INTENSITY,
  449. # Low intensity.
  450. "ansired": color_cls.RED,
  451. "ansigreen": color_cls.GREEN,
  452. "ansiyellow": color_cls.YELLOW,
  453. "ansiblue": color_cls.BLUE,
  454. "ansimagenta": color_cls.MAGENTA,
  455. "ansicyan": color_cls.CYAN,
  456. # High intensity.
  457. "ansibrightred": color_cls.RED | color_cls.INTENSITY,
  458. "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY,
  459. "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY,
  460. "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY,
  461. "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY,
  462. "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY,
  463. }
  464. FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR)
  465. BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR)
  466. assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  467. assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  468. class ColorLookupTable:
  469. """
  470. Inspired by pygments/formatters/terminal256.py
  471. """
  472. def __init__(self) -> None:
  473. self._win32_colors = self._build_color_table()
  474. # Cache (map color string to foreground and background code).
  475. self.best_match: dict[str, tuple[int, int]] = {}
  476. @staticmethod
  477. def _build_color_table() -> list[tuple[int, int, int, int, int]]:
  478. """
  479. Build an RGB-to-256 color conversion table
  480. """
  481. FG = FOREGROUND_COLOR
  482. BG = BACKGROUND_COLOR
  483. return [
  484. (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK),
  485. (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE),
  486. (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN),
  487. (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN),
  488. (0xAA, 0x00, 0x00, FG.RED, BG.RED),
  489. (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA),
  490. (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW),
  491. (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY),
  492. (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY),
  493. (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY),
  494. (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY),
  495. (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY),
  496. (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY),
  497. (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY),
  498. (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY),
  499. (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY),
  500. ]
  501. def _closest_color(self, r: int, g: int, b: int) -> tuple[int, int]:
  502. distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
  503. fg_match = 0
  504. bg_match = 0
  505. for r_, g_, b_, fg_, bg_ in self._win32_colors:
  506. rd = r - r_
  507. gd = g - g_
  508. bd = b - b_
  509. d = rd * rd + gd * gd + bd * bd
  510. if d < distance:
  511. fg_match = fg_
  512. bg_match = bg_
  513. distance = d
  514. return fg_match, bg_match
  515. def _color_indexes(self, color: str) -> tuple[int, int]:
  516. indexes = self.best_match.get(color, None)
  517. if indexes is None:
  518. try:
  519. rgb = int(str(color), 16)
  520. except ValueError:
  521. rgb = 0
  522. r = (rgb >> 16) & 0xFF
  523. g = (rgb >> 8) & 0xFF
  524. b = rgb & 0xFF
  525. indexes = self._closest_color(r, g, b)
  526. self.best_match[color] = indexes
  527. return indexes
  528. def lookup_fg_color(self, fg_color: str) -> int:
  529. """
  530. Return the color for use in the
  531. `windll.kernel32.SetConsoleTextAttribute` API call.
  532. :param fg_color: Foreground as text. E.g. 'ffffff' or 'red'
  533. """
  534. # Foreground.
  535. if fg_color in FG_ANSI_COLORS:
  536. return FG_ANSI_COLORS[fg_color]
  537. else:
  538. return self._color_indexes(fg_color)[0]
  539. def lookup_bg_color(self, bg_color: str) -> int:
  540. """
  541. Return the color for use in the
  542. `windll.kernel32.SetConsoleTextAttribute` API call.
  543. :param bg_color: Background as text. E.g. 'ffffff' or 'red'
  544. """
  545. # Background.
  546. if bg_color in BG_ANSI_COLORS:
  547. return BG_ANSI_COLORS[bg_color]
  548. else:
  549. return self._color_indexes(bg_color)[1]