From db4c8d91eaf091ef18809b87e8431bfcab76352c Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 29 Jan 2026 08:58:08 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 6 + .../tools/mirb/mirb_highlight.c | 45 +++++- .../tools/mirb/mirb_highlight.h | 8 +- mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c | 141 ++++++++++++++++++ mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h | 16 ++ 5 files changed, 209 insertions(+), 7 deletions(-) diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 8f475122f..92f625fc6 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -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); diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c index 3d1c4f1f1..3d6826a8b 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c @@ -5,6 +5,8 @@ */ #include "mirb_highlight.h" +#include "mirb_term.h" +#include #include #include #include @@ -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; } diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h index b81a23845..505d211e6 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h @@ -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 diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c index ced2b4c2f..68759e262 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c @@ -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 */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h index 6995bb15f..8c9c5d1f2 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h @@ -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 */