chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify light-theme detection end to end.
|
||||
|
||||
Spawns the freshly built jcode TUI in a PTY, replies to terminal queries
|
||||
(OSC 10/11 foreground/background, DA, cursor position) advertising either a
|
||||
white or black background, captures the rendered output, and compares the SGR
|
||||
color usage. On the white background run, jcode should detect a light theme
|
||||
and emit dark foreground colors; on the black run it should keep the normal
|
||||
light-on-dark palette.
|
||||
"""
|
||||
|
||||
import os
|
||||
import fcntl
|
||||
import pty
|
||||
import re
|
||||
import struct
|
||||
import termios
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
JCODE = os.path.expanduser("~/.jcode/builds/current/jcode")
|
||||
|
||||
SGR_FG_RGB = re.compile(rb"38;2;(\d+);(\d+);(\d+)[;m]")
|
||||
SGR_FG_256 = re.compile(rb"\x1b\[38;5;(\d+)m")
|
||||
|
||||
|
||||
def reply_queries(master_fd: int, buffer: bytes, bg: str) -> bytes:
|
||||
# bg is "ffff" (white) or "0000" (black) per channel
|
||||
fg = b"0000" if bg == "ffff" else b"ffff"
|
||||
bgb = bg.encode()
|
||||
replies = [
|
||||
(b"\x1b[6n", b"\x1b[1;1R"),
|
||||
(b"\x1b[c", b"\x1b[?62;c"),
|
||||
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:%s/%s/%s\x1b\\" % ((fg,) * 3)),
|
||||
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:%s/%s/%s\x1b\\" % ((bgb,) * 3)),
|
||||
(b"\x1b]10;?\x07", b"\x1b]10;rgb:%s/%s/%s\x07" % ((fg,) * 3)),
|
||||
(b"\x1b]11;?\x07", b"\x1b]11;rgb:%s/%s/%s\x07" % ((bgb,) * 3)),
|
||||
(b"\x1b[14t", b"\x1b[4;600;800t"),
|
||||
(b"\x1b[16t", b"\x1b[6;16;8t"),
|
||||
(b"\x1b[18t", b"\x1b[8;24;80t"),
|
||||
]
|
||||
# Reply in the order queries appear in the stream: terminals process
|
||||
# escape sequences sequentially, and colorsaurus relies on the OSC
|
||||
# responses arriving before the DA1 response.
|
||||
while True:
|
||||
first = None
|
||||
for query, response in replies:
|
||||
idx = buffer.find(query)
|
||||
if idx != -1 and (first is None or idx < first[0]):
|
||||
first = (idx, query, response)
|
||||
if first is None:
|
||||
return buffer
|
||||
_, query, response = first
|
||||
os.write(master_fd, response)
|
||||
buffer = buffer.replace(query, b"", 1)
|
||||
|
||||
|
||||
def run_capture(bg: str, seconds: float = 12.0) -> bytes:
|
||||
master, slave = pty.openpty()
|
||||
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 800, 600))
|
||||
env = dict(os.environ)
|
||||
env["TERM"] = "xterm-256color"
|
||||
env["COLORTERM"] = "truecolor"
|
||||
for k in ("JCODE_THEME", "TERM_PROGRAM", "TERM_PROGRAM_VERSION", "TMUX", "STY", "SSH_TTY", "SSH_CONNECTION", "SSH_CLIENT", "KITTY_WINDOW_ID", "GHOSTTY_RESOURCES_DIR", "WEZTERM_PANE", "ZELLIJ"):
|
||||
env.pop(k, None)
|
||||
proc = subprocess.Popen(
|
||||
[JCODE],
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=slave,
|
||||
env=env,
|
||||
close_fds=True,
|
||||
)
|
||||
os.close(slave)
|
||||
out = b""
|
||||
buf = b""
|
||||
deadline = time.time() + seconds
|
||||
while time.time() < deadline:
|
||||
r, _, _ = select.select([master], [], [], 0.2)
|
||||
if master in r:
|
||||
try:
|
||||
chunk = os.read(master, 65536)
|
||||
except OSError:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
out += chunk
|
||||
buf += chunk
|
||||
buf = reply_queries(master, buf, bg)
|
||||
try:
|
||||
os.write(master, b"\x03\x03") # ctrl-c twice to quit
|
||||
time.sleep(0.5)
|
||||
except OSError:
|
||||
pass
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
os.close(master)
|
||||
return out
|
||||
|
||||
|
||||
def fg_colors(raw: bytes) -> set[tuple[int, int, int]]:
|
||||
return {(int(r), int(g), int(b)) for r, g, b in SGR_FG_RGB.findall(raw)}
|
||||
|
||||
|
||||
def lum(c: tuple[int, int, int]) -> float:
|
||||
r, g, b = c
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("capturing with black background reply (expect dark theme)...")
|
||||
dark_raw = run_capture("0000")
|
||||
print("capturing with white background reply (expect light theme)...")
|
||||
light_raw = run_capture("ffff")
|
||||
|
||||
dark = fg_colors(dark_raw)
|
||||
light = fg_colors(light_raw)
|
||||
print(f"dark-bg run: {len(dark)} distinct truecolor fg colors")
|
||||
print(f"light-bg run: {len(light)} distinct truecolor fg colors")
|
||||
|
||||
if len(dark) < 3 or len(light) < 3:
|
||||
print("FAIL: not enough SGR samples captured; TUI may not have rendered")
|
||||
return 1
|
||||
|
||||
# jcode's dim_color() is rgb(80,80,80); its lightness flip is (175,175,175).
|
||||
# The dark run must use the native palette and the light run the flipped one.
|
||||
if (80, 80, 80) not in dark:
|
||||
print("FAIL: dark run missing native dim color (80,80,80)")
|
||||
return 1
|
||||
if (175, 175, 175) not in light:
|
||||
print(f"FAIL: light run missing flipped dim color (175,175,175); got {sorted(light)}")
|
||||
return 1
|
||||
if (80, 80, 80) in light:
|
||||
print("FAIL: light run still emits native dim color (80,80,80)")
|
||||
return 1
|
||||
|
||||
# Bright accent colors must darken: the light run should contain colors that
|
||||
# are darker than anything in the dark run's brighter half.
|
||||
darkest_light = min(map(lum, light))
|
||||
if darkest_light > 0.25:
|
||||
print(f"FAIL: light run has no dark accents (min luminance {darkest_light:.2f})")
|
||||
return 1
|
||||
|
||||
print("PASS: white background detected; palette flipped for light theme")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user