From fb158f81a2ef36b0efae07bcf1c2593492590964 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 15:33:58 +0100 Subject: [PATCH 1/8] detect windows --- rich/_windows.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ rich/console.py | 49 ++++++++++++++++++++++++++---------- rich/style.py | 4 +-- 3 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 rich/_windows.py diff --git a/rich/_windows.py b/rich/_windows.py new file mode 100644 index 00000000..ef2e9204 --- /dev/null +++ b/rich/_windows.py @@ -0,0 +1,64 @@ +import sys + +from dataclasses import dataclass + + +@dataclass +class WindowsConsoleFeatures: + """Windows features available.""" + + vt: bool + truecolor: bool + + +try: + import ctypes + from ctypes import wintypes + from ctypes import LibraryLoader + + windll = LibraryLoader(ctypes.WinDLL) +except (AttributeError, ImportError): + + # Fallback if we can't load the Windows DLL + def get_windows_console_features() -> WindowsConsoleFeatures: + features = WindowsConsoleFeatures() + return features + + +else: + + STDOUT = -11 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + _GetConsoleMode = windll.kernel32.GetConsoleMode + _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] + _GetConsoleMode.restype = wintypes.BOOL + + _GetStdHandle = windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [ + wintypes.DWORD, + ] + _GetStdHandle.restype = wintypes.HANDLE + + def get_windows_console_features() -> WindowsConsoleFeatures: + """Get windows console fatures. + + Returns: + WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. + """ + handle = _GetStdHandle(STDOUT) + console_mode = wintypes.DWORD() + result = _GetConsoleMode(handle, console_mode) + vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + truecolor = False + if vt: + win_version = sys.getwindowsversion() + truecolor = win_version.major > 10 or ( + win_version.major == 10 and win_version.build > 15063 + ) + features = WindowsConsoleFeatures(vt=vt, truecolor=truecolor) + return features + + +if __name__ == "__main__": + print(get_windows_console_features()) + diff --git a/rich/console.py b/rich/console.py index 6119ef89..78efe71b 100644 --- a/rich/console.py +++ b/rich/console.py @@ -21,6 +21,7 @@ from typing import ( Iterable, List, Optional, + TYPE_CHECKING, NamedTuple, Union, ) @@ -44,6 +45,8 @@ from .segment import Segment from .text import Text, TextType from .theme import Theme +if TYPE_CHECKING: + from ._windows import WindowsConsoleFeatures WINDOWS = platform.system() == "Windows" @@ -244,9 +247,22 @@ class RenderHook: return renderables +_windows_console_features: Optional["WindowsConsoleFeatures"] = None + + +def get_windows_console_features() -> "WindowsConsoleFeatures": + global _windows_console_features + if _windows_console_features is not None: + return _windows_console_features + from ._windows import get_windows_console_features + + _windows_console_features = get_windows_console_features() + return _windows_console_features + + def detect_legacy_windows() -> bool: """Detect legacy Windows.""" - return "WINDIR" in os.environ and "WT_SESSION" not in os.environ + return WINDOWS and not get_windows_console_features().vt if detect_legacy_windows(): # pragma: no cover @@ -364,19 +380,26 @@ class Console: def _detect_color_system(self) -> Optional[ColorSystem]: """Detect color system from env vars.""" - if not self.is_terminal: + if not self.is_terminal or "NO_COLOR" in os.environ: return None - if self.legacy_windows: # pragma: no cover - return ColorSystem.WINDOWS - if "WT_SESSION" in os.environ: - # Exception for Windows terminal - return ColorSystem.TRUECOLOR - color_term = os.environ.get("COLORTERM", "").strip().lower() - return ( - ColorSystem.TRUECOLOR - if color_term in ("truecolor", "24bit") - else ColorSystem.EIGHT_BIT - ) + if WINDOWS: # pragma: no cover + if self.legacy_windows: # pragma: no cover + return ColorSystem.WINDOWS + windows_console_features = get_windows_console_features() + return ( + ColorSystem.TRUECOLOR + if windows_console_features.truecolor + else ColorSystem.EIGHT_BIT + ) + else: + if self.is_jupyter: + return ColorSystem.TRUECOLOR + color_term = os.environ.get("COLORTERM", "").strip().lower() + return ( + ColorSystem.TRUECOLOR + if color_term in ("truecolor", "24bit") + else ColorSystem.EIGHT_BIT + ) def _enter_buffer(self) -> None: """Enter in to a buffer context, and buffer all output.""" diff --git a/rich/style.py b/rich/style.py index cf46a684..1277ddd8 100644 --- a/rich/style.py +++ b/rich/style.py @@ -247,9 +247,9 @@ class Style: for bit in range(9, 13): if attributes & (1 << bit): append(_style_map[bit]) - if self._color is not None: + if self._color is not None and color_system: sgr.extend(self._color.downgrade(color_system).get_ansi_codes()) - if self._bgcolor is not None: + if self._bgcolor is not None and color_system: sgr.extend( self._bgcolor.downgrade(color_system).get_ansi_codes( foreground=False From e0d1c6315a75143b736767ff185f281661ab98a9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 15:36:55 +0100 Subject: [PATCH 2/8] type fix --- rich/_windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rich/_windows.py b/rich/_windows.py index ef2e9204..e85f4c47 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -7,8 +7,8 @@ from dataclasses import dataclass class WindowsConsoleFeatures: """Windows features available.""" - vt: bool - truecolor: bool + vt: bool = False + truecolor: bool = False try: @@ -16,7 +16,7 @@ try: from ctypes import wintypes from ctypes import LibraryLoader - windll = LibraryLoader(ctypes.WinDLL) + windll = LibraryLoader(ctypes.WinDLL) # type: ignore except (AttributeError, ImportError): # Fallback if we can't load the Windows DLL From 7bf9dd8f738436407dfb57fd2b940f531a0fbbe5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 15:56:27 +0100 Subject: [PATCH 3/8] typing, jupyter fix --- .coveragerc | 1 + rich/console.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index 1d1c6c16..b4d38778 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,6 @@ [run] omit = rich/jupyter.py + rich/_windows.py [report] exclude_lines = diff --git a/rich/console.py b/rich/console.py index 78efe71b..498b7fe8 100644 --- a/rich/console.py +++ b/rich/console.py @@ -250,7 +250,7 @@ class RenderHook: _windows_console_features: Optional["WindowsConsoleFeatures"] = None -def get_windows_console_features() -> "WindowsConsoleFeatures": +def get_windows_console_features() -> "WindowsConsoleFeatures": # pragma: no cover global _windows_console_features if _windows_console_features is not None: return _windows_console_features @@ -380,6 +380,8 @@ class Console: def _detect_color_system(self) -> Optional[ColorSystem]: """Detect color system from env vars.""" + if self.is_jupyter: + return ColorSystem.TRUECOLOR if not self.is_terminal or "NO_COLOR" in os.environ: return None if WINDOWS: # pragma: no cover @@ -392,8 +394,6 @@ class Console: else ColorSystem.EIGHT_BIT ) else: - if self.is_jupyter: - return ColorSystem.TRUECOLOR color_term = os.environ.get("COLORTERM", "").strip().lower() return ( ColorSystem.TRUECOLOR @@ -944,7 +944,7 @@ class Console: """Check if the buffer may be rendered.""" with self._lock: if self._buffer_index == 0: - if self.is_jupyter: + if self.is_jupyter: # pragma: no cover from .jupyter import display display(self._buffer) From 437a9ceeb4f83234d7a702ced88b3843783e7787 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 16:08:47 +0100 Subject: [PATCH 4/8] version bump --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70907036..1bdcdaf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.4] - 2020-07-07 + +### Changed + +- More precise detection of Windows console https://github.com/willmcgugan/rich/issues/140 + ## [3.0.3] - 2020-07-03 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 8ebacf37..1e6ad0cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rich" homepage = "https://github.com/willmcgugan/rich" documentation = "https://rich.readthedocs.io/en/latest/" -version = "3.0.3" +version = "3.0.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" From 739255b32eb4287e1a5e894696119a50830c72f9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 16:26:25 +0100 Subject: [PATCH 5/8] tests --- rich/_windows.py | 5 ++++- rich/highlighter.py | 2 +- tests/test_jupyter.py | 7 +++++++ tests/test_log.py | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 tests/test_jupyter.py diff --git a/rich/_windows.py b/rich/_windows.py index e85f4c47..bb051e97 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -60,5 +60,8 @@ else: if __name__ == "__main__": - print(get_windows_console_features()) + features = get_windows_console_features() + from rich import print + + print(repr(features)) diff --git a/rich/highlighter.py b/rich/highlighter.py index 2b55fcc3..758ddad0 100644 --- a/rich/highlighter.py +++ b/rich/highlighter.py @@ -73,7 +73,7 @@ class ReprHighlighter(RegexHighlighter): highlights = [ r"(?P[\{\[\(\)\]\}])", r"(?P\<)(?P\w*)(?P.*?)(?P\>)", - r"(?P\w+?)=(?P\"?\S+\"?)", + r"(?P\w+?)=(?P\"?\S+?\"?)", r"(?PTrue)|(?PFalse)|(?PNone)", r"(?P(?0x[0-9a-f]*)", diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py new file mode 100644 index 00000000..a69f211b --- /dev/null +++ b/tests/test_jupyter.py @@ -0,0 +1,7 @@ +from rich.console import Console + + +def test_jupyter(): + console = Console(force_jupyter=True) + assert console.width == 93 + assert console.color_system == "truecolor" diff --git a/tests/test_log.py b/tests/test_log.py index edafaa02..28098ae7 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -27,7 +27,7 @@ def render_log(): def test_log(): - expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n" + expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m8\x1b[0m\x1b[1;34m0\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m8\x1b[0m\x1b[1;34m0\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n" assert render_log() == expected From 8403a5627b7e41dc621b854b54ac45c475923737 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 16:49:30 +0100 Subject: [PATCH 6/8] windows fix --- rich/_windows.py | 5 ++++- rich/highlighter.py | 2 +- tests/test_log.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rich/_windows.py b/rich/_windows.py index bb051e97..656644a0 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -17,7 +17,7 @@ try: from ctypes import LibraryLoader windll = LibraryLoader(ctypes.WinDLL) # type: ignore -except (AttributeError, ImportError): +except (AttributeError, ImportError, ValueError): # Fallback if we can't load the Windows DLL def get_windows_console_features() -> WindowsConsoleFeatures: @@ -60,8 +60,11 @@ else: if __name__ == "__main__": + import platform + features = get_windows_console_features() from rich import print + print(f'platform="{platform.system()}"') print(repr(features)) diff --git a/rich/highlighter.py b/rich/highlighter.py index 758ddad0..b4c1adc0 100644 --- a/rich/highlighter.py +++ b/rich/highlighter.py @@ -73,7 +73,7 @@ class ReprHighlighter(RegexHighlighter): highlights = [ r"(?P[\{\[\(\)\]\}])", r"(?P\<)(?P\w*)(?P.*?)(?P\>)", - r"(?P\w+?)=(?P\"?\S+?\"?)", + r"(?P\w+?)=(?P\"?[\w_]+\"?)", r"(?PTrue)|(?PFalse)|(?PNone)", r"(?P(?0x[0-9a-f]*)", diff --git a/tests/test_log.py b/tests/test_log.py index 28098ae7..edafaa02 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -27,7 +27,7 @@ def render_log(): def test_log(): - expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m8\x1b[0m\x1b[1;34m0\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m8\x1b[0m\x1b[1;34m0\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n" + expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n" assert render_log() == expected From 56432880402d23df43244960e0e6dc2d41baf373 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 16:51:06 +0100 Subject: [PATCH 7/8] typo --- rich/_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/_windows.py b/rich/_windows.py index 656644a0..3273bf74 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -40,7 +40,7 @@ else: _GetStdHandle.restype = wintypes.HANDLE def get_windows_console_features() -> WindowsConsoleFeatures: - """Get windows console fatures. + """Get windows console features. Returns: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. From d8a3a72c50d36350da999b9ed9919f1059479022 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 7 Jul 2020 17:00:26 +0100 Subject: [PATCH 8/8] no none color_system --- rich/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/style.py b/rich/style.py index 1277ddd8..cf46a684 100644 --- a/rich/style.py +++ b/rich/style.py @@ -247,9 +247,9 @@ class Style: for bit in range(9, 13): if attributes & (1 << bit): append(_style_map[bit]) - if self._color is not None and color_system: + if self._color is not None: sgr.extend(self._color.downgrade(color_system).get_ansi_codes()) - if self._bgcolor is not None and color_system: + if self._bgcolor is not None: sgr.extend( self._bgcolor.downgrade(color_system).get_ansi_codes( foreground=False