Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically detect terminal background color #4674

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a policy in Textual of no abbreviations, so BG should be BACKGROUND.

I think this would be better done as a regex.

Do you have a reference to the docs? Can you be certain it is sent in a two hex characters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix the abbreviation. Here is a link to some documentation on the escape sequence:

https://www.xfree86.org/current/ctlseqs.html

Search for "Operating System Controls"

"""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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't want to do this automatically, unless the dev explicitly requests this behaviour.

Sending the message is probably fine, until we have a mechanism for the dev to request automatic light / dark mode.

"""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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK some terminals respond with nothing when they see a sequence they don't recognize/support, i.e. if you send requests <supported 1>;<unsupported 2>;<supported 3>; the responses will be <response 1>;<response 3> only. If anything expects order guarantee for requests it may end up waiting forever.
Is this case handled (I didn't do an in-depth review, just asking)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to handle this case gracefully by only parsing the RGB colors if the length is as expected. But, please take a good look at the code to ensure I do not have missed anything. I would also like to look into writing some unit tests for this, including the case you mention here

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
Loading