diff --git a/bin/sonic.js b/bin/sonic.js index 8b7525b..c6850b0 100755 --- a/bin/sonic.js +++ b/bin/sonic.js @@ -18,6 +18,7 @@ import { verifyLogin } from '../lib/spotify.js'; import { UI } from '../lib/ui.js'; +import { queryTerminalBackground } from '../lib/themes.js'; import { CHANNELS, ACTIVITIES, getSuggestedChannel, getSuggestedActivity, @@ -811,7 +812,11 @@ function promptSearch(notice = '') { }); } -process.stdin.on('data', async (key) => { +process.stdin.on('data', async (input) => { + const key = String(input); + // A late reply to the background-color query is not a keypress. + if (key.startsWith('\x1b]')) return; + if (key === '\x03') { cleanup(0); return; @@ -1144,6 +1149,9 @@ async function main() { process.exit(1); } + // Learn light vs dark before the first frame, so text never lands white on white. + if (store.config.colorMode === 'auto') await queryTerminalBackground(); + ui.hideCursor(); if (connectMode) { diff --git a/lib/themes.js b/lib/themes.js index 9a0a55e..c546fde 100644 --- a/lib/themes.js +++ b/lib/themes.js @@ -242,6 +242,37 @@ export function cycleTheme(themeId, direction = 1) { let _detectedMode = null; +// Ask the terminal for its background color (OSC 11). Most terminals answer +// within a few ms; Terminal.app sets no COLORFGBG, so this is the reliable path. +export function queryTerminalBackground(timeoutMs = 250) { + return new Promise((resolve) => { + if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(null); + const wasRaw = process.stdin.isRaw; + let buffer = ''; + const finish = (mode) => { + clearTimeout(timer); + process.stdin.off('data', onData); + if (!wasRaw) process.stdin.setRawMode(false); + process.stdin.pause(); + if (mode) _detectedMode = mode; + resolve(mode); + }; + const onData = (chunk) => { + buffer += chunk.toString(); + const match = buffer.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i); + if (!match) return; + const [r, g, b] = match.slice(1).map((hex) => parseInt(hex, 16) / (16 ** hex.length - 1)); + const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + finish(luminance > 0.5 ? 'light' : 'dark'); + }; + const timer = setTimeout(() => finish(null), timeoutMs); + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onData); + process.stdout.write('\x1b]11;?\x07'); + }); +} + export function detectColorMode() { if (_detectedMode) return _detectedMode; const colorfgbg = process.env.COLORFGBG;