From 21924234bd08f632a0243defce806e8d542c0a99 Mon Sep 17 00:00:00 2001 From: David Hallas Date: Tue, 25 Jun 2024 09:53:49 +0200 Subject: [PATCH] Automatically detect terminal background color Add support for automatically detecting the background color of the terminal using operating system command escape sequence. The response is then parsed and used to determine if the background is light or dark. --- src/textual/_xterm_parser.py | 19 ++++++++++++++++++- src/textual/app.py | 9 +++++++++ src/textual/drivers/linux_driver.py | 1 + src/textual/events.py | 11 +++++++++++ tests/test_xterm_parser.py | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 5b810273fa..f4379ff73a 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -14,7 +14,7 @@ # When trying to determine whether the current sequence is a supported/valid # escape sequence, at which length should we give up and consider our search # to be unsuccessful? -_MAX_SEQUENCE_SEARCH_THRESHOLD = 20 +_MAX_SEQUENCE_SEARCH_THRESHOLD = 24 _re_mouse_event = re.compile("^" + re.escape("\x1b[") + r"( None: bracketed_paste = False break + if sequence.startswith(BG_COLOR) and len(sequence) == BG_COLOR_LEN: + rgb_str = sequence[len(BG_COLOR) :] + rgb = rgb_str.split("/") + if len(rgb) == 3: + r = int(rgb[0], 16) + g = int(rgb[1], 16) + b = int(rgb[2], 16) + self.debug_log( + f"Detected BG_COLOR response {r:02x}/{g:02x}/{b:02x}" + ) + on_token(events.BackgroundColor(r, g, b)) + break + if not bracketed_paste: # Check cursor position report if ( diff --git a/src/textual/app.py b/src/textual/app.py index f34ca20370..354d400f9f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3275,6 +3275,15 @@ async def _on_app_blur(self, event: events.AppBlur) -> None: self.app_focus = False self.screen.refresh_bindings() + def _is_dark_color(self, r: int, g: int, b: int) -> bool: + # perceived brightness formula + perceived_brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b + return perceived_brightness < 128 + + async def _on_background_color(self, event: events.BackgroundColor) -> None: + """Background color detected""" + self.dark = self._is_dark_color(event.r & 0xFF, event.g & 0xFF, event.b & 0xFF) + def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]: """Detach a list of widgets from the DOM. diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index cd6a4c0130..835ba63675 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -248,6 +248,7 @@ def on_terminal_resize(signum, stack) -> None: self.flush() self._key_thread = Thread(target=self._run_input_thread) send_size_event() + self.write("\x1b]11;?\x07") # Detect background color self._key_thread.start() self._request_terminal_sync_mode_support() self._enable_bracketed_paste() diff --git a/src/textual/events.py b/src/textual/events.py index ce5ecac4e3..9a2bece489 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -707,3 +707,14 @@ def __init__(self, text: str, stderr: bool = False) -> None: def __rich_repr__(self) -> rich.repr.Result: yield self.text yield self.stderr + + +@dataclass +class BackgroundColor(Event, bubble=False): + """Internal event used when background color of the terminal is + detected + """ + + r: int + g: int + b: int diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 2995738897..92ad77be14 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -91,7 +91,7 @@ def test_cant_match_escape_sequence_too_long(parser): """The sequence did not match, and we hit the maximum sequence search length threshold, so each character should be issued as a key-press instead. """ - sequence = "\x1b[123456789123456789123" + sequence = "\x1b[123456789123456789123456" events = list(parser.feed(sequence)) # Every character in the sequence is converted to a key press