Module: Tuile::TerminalBackground

Defined in:
lib/tuile/terminal_background.rb,
sig/tuile.rbs

Overview

Detects the terminal's background color — both the light/dark scheme Screen picks Tuile::Theme::LIGHT or Tuile::Theme::DARK from, and, when the terminal answers the query, the actual RGB behind it.

Two mechanisms, in order of reliability:

  1. OSC 11 query — writes ESC ] 11 ; ? BEL to the terminal; modern terminals (xterm, kitty, alacritty, wezterm, iTerm2, GNOME Terminal, Windows Terminal) reply on stdin with the background color (\e]11;rgb:RRRR/GGGG/BBBB + BEL or ST). The color's relative luminance against a 0.5 threshold decides light vs dark. Terminals that don't support the query simply never reply, so the read is bounded by a short timeout.
  2. COLORFGBG env var — rxvt/konsole export "fg;bg" ANSI palette indices. Less reliable (stale across SSH/tmux, often unset); used only when OSC 11 yields nothing. Palette indices carry no RGB, so this path fills in Result#scheme and leaves Result#color nil.

Timing matters: the OSC 11 reply arrives on stdin, so the query must complete before EventQueue#start_key_thread owns stdin — otherwise the reply bytes get consumed as garbage keystrokes. Screen calls TerminalBackground.detect from its constructor, which apps run before Screen#run_event_loop; don't call this after the event loop started.

Once the loop is running the query is still available, from the other side: Screen writes QUERY on every OS appearance flip and the key thread — which owns stdin by then — reads the reply back through Keys.getkey and TerminalBackground.parse. See Screen#background_color.

Defined Under Namespace

Classes: Result

Constant Summary collapse

QUERY_TIMEOUT =

How long to wait for the OSC 11 reply. Generous for a local terminal; bounded so unsupporting terminals (which never reply) don't stall startup.

Returns:

  • (Float)

    seconds.

0.1
QUERY =

The OSC 11 background-color query, BEL-terminated.

Returns:

  • (String)
"\e]11;?\a"
REPLY =

Matches the OSC 11 reply. Components are 1–4 hex digits each (terminals vary); rgba: (4 components) also matches — the alpha tail is ignored.

Returns:

  • (Regexp)
%r{\e\]11;rgba?:(\h{1,4})/(\h{1,4})/(\h{1,4})}
NOTIFY_ON =

Enables mode 2031: the terminal pushes a color-scheme report (\e[?997;1n dark / \e[?997;2n light) whenever the OS appearance flips — see EventQueue::ColorSchemeEvent. Terminals without support ignore the sequence. Written by Screen#run_event_loop.

Returns:

  • (String)
"\e[?2031h"
NOTIFY_OFF =

Disables mode 2031 again; written when the event loop exits.

Returns:

  • (String)
"\e[?2031l"

Class Method Summary collapse

Class Method Details

.detect(input: $stdin, output: $stdout, env: ENV, timeout: QUERY_TIMEOUT) ⇒ Object

Detects the terminal background. Queries OSC 11 when both input and output are TTYs, falling back to COLORFGBG.

TerminalBackground.detect
# => #<data Result scheme=:dark, color=#<Tuile::Color [30, 30, 46]>>

@param input — where the OSC 11 reply arrives (the TTY input).

@param output — where the query is written (the TTY output).

@param env — environment for the COLORFGBG fallback; defaults to ENV (which duck-types the [] lookup).

@param timeout — max seconds to wait for the OSC 11 reply.

@return — nil when the background is undetectable — neither mechanism answered.



88
89
90
91
92
93
94
# File 'lib/tuile/terminal_background.rb', line 88

def detect(input: $stdin, output: $stdout, env: ENV, timeout: QUERY_TIMEOUT)
  osc = query_osc11(input, output, timeout) if input.tty? && output.tty?
  return osc if osc

  scheme = from_colorfgbg(env["COLORFGBG"])
  scheme && Result.new(scheme: scheme, color: nil)
end

.parse(reply) ⇒ Result?

Parses an OSC 11 reply — the terminal's answer to QUERY, matched anywhere in reply.

TerminalBackground.parse("\e]11;rgb:1e1e/1e1e/2e2e\a").color
# => #<Tuile::Color [30, 30, 46]>

Public because a reply also arrives mid-session, long after detect's own bounded read: once the key thread owns stdin, a whole reply surfaces as one "key" from Keys.getkey.

@param reply — raw terminal output that may contain a reply.

@return — nil when reply holds no OSC 11 reply.

Parameters:

  • reply (String)

Returns:



108
109
110
111
112
113
114
115
116
# File 'lib/tuile/terminal_background.rb', line 108

def parse(reply)
  match = REPLY.match(reply)
  return nil unless match

  # Components arrive as 1–4 hex digits (terminals vary), so each is
  # scaled by its own width: "ab" and "abab" are both ~0.67.
  components = match.captures.map { |c| c.to_i(16).fdiv((16**c.length) - 1) }
  Result.new(scheme: classify(components), color: to_color(components))
end