Skip to content

Commit

Permalink
Automatically detect terminal background color
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dhallas committed Jun 25, 2024
1 parent c9c34c7 commit 2192423
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 2 deletions.
19 changes: 18 additions & 1 deletion src/textual/_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"(<?[\d;]+[mM]|M...)\Z")
_re_terminal_mode_response = re.compile(
Expand All @@ -31,6 +31,10 @@
"""Sequence received when the terminal receives focus."""
FOCUSOUT: Final[str] = "\x1b[O"
"""Sequence received when focus is lost from the terminal."""
BG_COLOR: Final[str] = "\x1b]11;rgb:"
"""Sequence received with information on terminal background color"""
BG_COLOR_LEN: Final[int] = len(BG_COLOR) + 14
"""Length of background color sequence"""

_re_extended_key: Final = re.compile(r"\x1b\[(?:(\d+)(?:;(\d+))?)?([u~ABCDEFHPQRS])")

Expand Down Expand Up @@ -239,6 +243,19 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> 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 (
Expand Down
9 changes: 9 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/textual/drivers/linux_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2192423

Please sign in to comment.