mruby-bin-mirb: add OSC 11 terminal background color detection

Automatically detect terminal background color using OSC 11 escape
sequence to select appropriate syntax highlighting theme (dark/light).

Detection priority: MIRB_THEME env > OSC 11 > COLORFGBG env > dark default.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-01-29 08:58:08 +09:00
parent e05bd8f806
commit db4c8d91ea
5 changed files with 209 additions and 7 deletions
+6
View File
@@ -37,6 +37,7 @@
#include "mirb_editor.h"
#include "mirb_completion.h"
#include "mirb_highlight.h"
/* obsolete configuration */
#ifdef DISABLE_MIRB_UNDERSCORE
@@ -520,6 +521,11 @@ main(int argc, char **argv)
mrb_define_global_const(mrb, "ARGV", ARGV);
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$DEBUG"), mrb_bool_value(args.debug));
/* Query terminal background color before any output */
if (isatty(fileno(stdin)) && isatty(fileno(stdout))) {
mirb_highlight_query_terminal();
}
print_hint();
cxt = mrb_ccontext_new(mrb);
@@ -5,6 +5,8 @@
*/
#include "mirb_highlight.h"
#include "mirb_term.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
@@ -140,35 +142,66 @@ print_colored(mirb_highlighter *hl, const char *start, size_t len, mirb_token_ty
if (*color) printf("%s", reset);
}
/* Cached terminal background color from pre-query */
static mirb_bg_color cached_bg_color = MIRB_BG_UNKNOWN;
static mrb_bool bg_color_queried = FALSE;
/*
* Detect theme from environment
* Pre-query terminal background color
* Must be called before any output to avoid response appearing on screen
*/
void
mirb_highlight_query_terminal(void)
{
if (!bg_color_queried) {
cached_bg_color = mirb_term_query_bg_color(500); /* 500ms timeout */
bg_color_queried = TRUE;
}
}
/*
* Detect theme from terminal background color
*
* Priority:
* 1. MIRB_THEME environment variable (explicit override)
* 2. Cached OSC 11 result (from mirb_highlight_query_terminal)
* 3. COLORFGBG environment variable (rxvt, some xterm)
* 4. Default to dark theme
*/
mirb_theme
mirb_highlight_detect_theme(void)
{
const char *env;
/* Check explicit MIRB_THEME first */
/* 1. Check explicit MIRB_THEME first (user override) */
env = getenv("MIRB_THEME");
if (env) {
if (strcmp(env, "light") == 0) return MIRB_THEME_LIGHT;
if (strcmp(env, "dark") == 0) return MIRB_THEME_DARK;
}
/* Check COLORFGBG (format: "fg;bg" where bg > 6 usually means light) */
/* 2. Use cached OSC 11 result (must call mirb_highlight_query_terminal first) */
if (cached_bg_color == MIRB_BG_LIGHT) return MIRB_THEME_LIGHT;
if (cached_bg_color == MIRB_BG_DARK) return MIRB_THEME_DARK;
/* 3. Check COLORFGBG (format: "fg;bg" where bg > 6 usually means light) */
env = getenv("COLORFGBG");
if (env) {
const char *semi = strchr(env, ';');
if (semi) {
int bg = atoi(semi + 1);
int bg_color = atoi(semi + 1);
/* Background colors 7, 15, or high values typically mean light theme */
if (bg == 7 || bg == 15 || (bg >= 230 && bg <= 255)) {
if (bg_color == 7 || bg_color == 15 || (bg_color >= 230 && bg_color <= 255)) {
return MIRB_THEME_LIGHT;
}
/* Low values (0-6, 8) typically mean dark theme */
if (bg_color <= 8) {
return MIRB_THEME_DARK;
}
}
}
/* Default to dark theme (more common in terminals) */
/* 4. Default to dark theme (more common in terminals) */
return MIRB_THEME_DARK;
}
@@ -59,11 +59,17 @@ void mirb_highlight_init(mirb_highlighter *hl, mrb_bool enabled);
void mirb_highlight_set_theme(mirb_highlighter *hl, mirb_theme theme);
/*
* Detect theme from environment variables
* Detect theme from environment variables and terminal query
* Returns MIRB_THEME_DARK if cannot detect
*/
mirb_theme mirb_highlight_detect_theme(void);
/*
* Pre-query terminal background color (call before any output)
* This caches the result for later use by mirb_highlight_detect_theme()
*/
void mirb_highlight_query_terminal(void);
/*
* Print a line with syntax highlighting
* Handles multi-line strings/comments by tracking state
@@ -348,3 +348,144 @@ mirb_term_clear_below(void)
{
printf("\033[J");
}
#if !defined(_WIN32) && !defined(_WIN64)
/*
* Query terminal background color using OSC 11 escape sequence
*
* Protocol:
* Send: ESC ] 11 ; ? ESC \ (or BEL instead of ESC \)
* Recv: ESC ] 11 ; rgb:RRRR/GGGG/BBBB ESC \
*
* The response uses 16-bit color values (0000-FFFF per component).
* Some terminals use 8-bit (00-FF) format instead.
*/
mirb_bg_color
mirb_term_query_bg_color(int timeout_ms)
{
struct termios old_term, new_term;
char buf[64];
size_t total = 0;
ssize_t n;
int r, g, b;
mirb_bg_color result = MIRB_BG_UNKNOWN;
fd_set fds;
struct timeval tv;
const char *p;
/* Must be a terminal */
if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
return MIRB_BG_UNKNOWN;
}
/* Save original terminal settings */
if (tcgetattr(STDIN_FILENO, &old_term) < 0) {
return MIRB_BG_UNKNOWN;
}
/* Flush any pending input first */
tcflush(STDIN_FILENO, TCIFLUSH);
/* Disable echo and canonical mode for raw read */
new_term = old_term;
new_term.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL);
new_term.c_iflag &= ~(IXON | IXOFF | ICRNL);
new_term.c_cc[VMIN] = 0;
new_term.c_cc[VTIME] = 0;
/* TCSAFLUSH: flush I/O and apply settings */
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term) < 0) {
return MIRB_BG_UNKNOWN;
}
/* Wait for terminal settings to take effect */
tcdrain(STDOUT_FILENO);
/* Send OSC 11 query: ESC ] 11 ; ? ESC \ */
if (write(STDOUT_FILENO, "\033]11;?\033\\", 8) != 8) {
goto restore;
}
/* Ensure query is sent to terminal */
tcdrain(STDOUT_FILENO);
/* Give terminal time to process and respond */
usleep(50000); /* 50ms */
/* Read response with timeout - loop to get complete response */
while (total < sizeof(buf) - 1) {
int sel;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
sel = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
if (sel < 0) {
if (errno == EINTR) continue; /* Retry on signal interrupt */
break; /* Other error */
}
if (sel == 0) break; /* Timeout */
n = read(STDIN_FILENO, buf + total, sizeof(buf) - 1 - total);
if (n <= 0) {
if (n < 0 && errno == EINTR) continue; /* Retry on signal interrupt */
break;
}
total += (size_t)n;
/* Check for terminator: ESC \ (0x1b 0x5c) or BEL (0x07) */
if (total >= 2 && buf[total-2] == '\033' && buf[total-1] == '\\') break;
if (total >= 1 && buf[total-1] == '\007') break;
/* Reduce timeout for subsequent reads */
timeout_ms = 10;
}
if (total == 0) {
goto restore;
}
buf[total] = '\0';
/* Parse response: look for "rgb:" followed by hex values */
p = strstr(buf, "rgb:");
if (p) {
p += 4;
/* Try 16-bit format: rgb:RRRR/GGGG/BBBB */
if (sscanf(p, "%4x/%4x/%4x", &r, &g, &b) == 3) {
/* Normalize to 8-bit range */
r >>= 8; g >>= 8; b >>= 8;
}
/* Try 8-bit format: rgb:RR/GG/BB */
else if (sscanf(p, "%2x/%2x/%2x", &r, &g, &b) == 3) {
/* Already 8-bit */
}
else {
goto restore;
}
/* Calculate relative luminance (ITU-R BT.709 simplified) */
/* Y = 0.2126*R + 0.7152*G + 0.0722*B */
/* Scale: 0-255 input, threshold at ~127.5 */
int luminance = (2126 * r + 7152 * g + 722 * b) / 10000;
result = (luminance < 128) ? MIRB_BG_DARK : MIRB_BG_LIGHT;
}
restore:
/* Flush any remaining input before restoring terminal */
tcflush(STDIN_FILENO, TCIFLUSH);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
return result;
}
#else /* Windows */
mirb_bg_color
mirb_term_query_bg_color(int timeout_ms)
{
(void)timeout_ms;
/* Windows Terminal supports OSC 11, but implementation requires
* different I/O handling. For now, return unknown and rely on
* fallback detection methods. */
return MIRB_BG_UNKNOWN;
}
#endif /* _WIN32 */
@@ -118,4 +118,20 @@ void mirb_term_get_size(mirb_term *term);
*/
void mirb_term_flush(void);
/*
* Background color detection result
*/
typedef enum mirb_bg_color {
MIRB_BG_UNKNOWN, /* could not detect */
MIRB_BG_DARK, /* dark background (luminance < 0.5) */
MIRB_BG_LIGHT /* light background (luminance >= 0.5) */
} mirb_bg_color;
/*
* Query terminal background color using OSC 11
* Returns MIRB_BG_UNKNOWN if terminal doesn't respond within timeout
* timeout_ms: timeout in milliseconds (recommended: 100)
*/
mirb_bg_color mirb_term_query_bg_color(int timeout_ms);
#endif /* MIRB_TERM_H */