mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 01:00:54 +00:00
fix(logs): adapt extended foreground colors to light pages
This commit is contained in:
1 parent
4f998ba7b4
commit
50d117ff6f
4 files changed
+48
-6
No files matched your search
@@ -77,7 +77,35 @@ function terminalPlainTextToHtml(plainText, hostLabel, timestamp) {
|
||||
return wrapTerminalHtmlContent(htmlContent, hostLabel, timestamp);
|
||||
}
|
||||
|
||||
function buildExtendedForegroundPalette(htmlContent) {
|
||||
const colors = new Set(Array.from(
|
||||
htmlContent.matchAll(/var\(--term-custom-([a-f0-9]{6}), #[a-f0-9]{6}\)/g),
|
||||
(match) => match[1],
|
||||
));
|
||||
const light = [];
|
||||
const dark = [];
|
||||
for (const hex of colors) {
|
||||
const channels = hex.match(/../g).map((channel) => parseInt(channel, 16));
|
||||
const luminance = (rgb) => rgb.reduce((sum, channel, index) => {
|
||||
const value = channel / 255;
|
||||
const linear = value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||
return sum + linear * [0.2126, 0.7152, 0.0722][index];
|
||||
}, 0);
|
||||
let adjusted = channels;
|
||||
// Scale all channels together to retain the hue. Stop at normal-text
|
||||
// contrast against the light page; dark mode retains the exact SGR color.
|
||||
for (let scale = 0.99; 1.05 / (luminance(adjusted) + 0.05) < 4.5; scale -= 0.01) {
|
||||
adjusted = channels.map((channel) => Math.floor(channel * Math.max(0, scale)));
|
||||
}
|
||||
const lightHex = adjusted.map((channel) => channel.toString(16).padStart(2, "0")).join("");
|
||||
light.push(`--term-custom-${hex}: #${lightHex};`);
|
||||
dark.push(`--term-custom-${hex}: #${hex};`);
|
||||
}
|
||||
return { light: light.join("\n "), dark: dark.join("\n ") };
|
||||
}
|
||||
|
||||
function wrapTerminalHtmlContent(htmlContent, hostLabel, timestamp) {
|
||||
const extendedPalette = buildExtendedForegroundPalette(htmlContent);
|
||||
const dateStr = new Date(timestamp).toLocaleString();
|
||||
const safeHostLabel = escapeHtml(hostLabel || "Unknown");
|
||||
const safeDateStr = escapeHtml(dateStr);
|
||||
@@ -113,6 +141,7 @@ function wrapTerminalHtmlContent(htmlContent, hostLabel, timestamp) {
|
||||
--ansi-13: #bc05bc;
|
||||
--ansi-14: #007c9b;
|
||||
--ansi-15: #767676;
|
||||
${extendedPalette.light}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@@ -136,6 +165,7 @@ function wrapTerminalHtmlContent(htmlContent, hostLabel, timestamp) {
|
||||
--ansi-13: #d670d6;
|
||||
--ansi-14: #29b8db;
|
||||
--ansi-15: #ffffff;
|
||||
${extendedPalette.dark}
|
||||
}
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -934,8 +934,12 @@ test("html wrapper adapts to light/dark browser color scheme", () => {
|
||||
});
|
||||
|
||||
test("all light-mode ANSI foregrounds remain readable on the default background", () => {
|
||||
const { wrapTerminalHtmlContent } = loadBridgeWithDialog({});
|
||||
const lightCss = wrapTerminalHtmlContent("hello", "host", 0).split("@media")[0];
|
||||
const { terminalDataToHtml } = loadBridgeWithDialog({});
|
||||
let data = "";
|
||||
for (let index = 0; index < 256; index++) data += `\x1b[38;5;${index}mcolor ${index}\x1b[0m\n`;
|
||||
data += "\x1b[38;2;255;255;255mwhite\x1b[0m\n\x1b[38;2;255;255;0myellow\x1b[0m\n\x1b[38;2;18;52;86mdark\x1b[0m";
|
||||
const html = terminalDataToHtml(data, "host", 0);
|
||||
const [lightCss, darkCss] = html.split("@media");
|
||||
const luminance = (hex) => {
|
||||
const channels = hex.match(/[a-f0-9]{2}/gi).map((channel) => {
|
||||
const value = parseInt(channel, 16) / 255;
|
||||
@@ -944,11 +948,16 @@ test("all light-mode ANSI foregrounds remain readable on the default background"
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||
};
|
||||
const background = luminance(lightCss.match(/--term-default-bg:\s*(#[a-f0-9]{6})/i)[1]);
|
||||
const palette = [...lightCss.matchAll(/--ansi-(\d+):\s*(#[a-f0-9]{6})/gi)];
|
||||
assert.equal(palette.length, 16);
|
||||
const palette = [...lightCss.matchAll(/--(ansi-\d+|term-custom-[a-f0-9]{6}):\s*(#[a-f0-9]{6})/gi)];
|
||||
assert.equal(palette.length, 257);
|
||||
for (const [, index, color] of palette) {
|
||||
const foreground = luminance(color);
|
||||
const contrast = (Math.max(background, foreground) + 0.05) / (Math.min(background, foreground) + 0.05);
|
||||
assert.ok(contrast >= 4.5, `ANSI ${index} (${color}) contrast ${contrast.toFixed(2)} is below 4.5:1`);
|
||||
}
|
||||
for (const [, hex, color] of lightCss.matchAll(/--term-custom-([a-f0-9]{6}):\s*(#[a-f0-9]{6})/gi)) {
|
||||
assert.ok(darkCss.includes(`--term-custom-${hex}: #${hex};`));
|
||||
if (1.05 / (luminance(hex) + 0.05) >= 4.5) assert.equal(color, `#${hex}`);
|
||||
}
|
||||
assert.match(html, /color: var\(--term-custom-ffffff, #ffffff\)[^>]*>white/);
|
||||
});
|
||||
@@ -606,6 +606,9 @@ function styleToCss(style) {
|
||||
const originalColor = (color) => color?.replace(/var\(--[\w-]+, (#[a-f0-9]{6})\)/g, "$1");
|
||||
fg = originalColor(fg) || DEFAULT_FOREGROUND;
|
||||
bg = originalColor(bg);
|
||||
} else if (fg?.startsWith("#")) {
|
||||
// The wrapper supplies a readable light variant for extended/true colors.
|
||||
fg = `var(--term-custom-${fg.slice(1)}, ${fg})`;
|
||||
}
|
||||
if (fg) declarations.push(`color: ${fg}`);
|
||||
if (bg) declarations.push(`background-color: ${bg}`);
|
||||
|
||||
@@ -260,12 +260,12 @@ test("SGR colors are emitted as themeable CSS variables with hex fallbacks", ()
|
||||
assert.match(html, /color:\s*#f14c4c; background-color:\s*#2472c8/);
|
||||
});
|
||||
|
||||
test("256-color base palette uses CSS variables; cube colors stay hex", () => {
|
||||
test("256-color base and cube foregrounds use themeable variables", () => {
|
||||
const renderer = createTerminalTextRenderer();
|
||||
renderer.feed("\x1b[38;5;1mbase\x1b[0m \x1b[38;5;208mcube\n");
|
||||
const html = renderer.toHtmlContent();
|
||||
assert.match(html, /color:\s*var\(--ansi-1, #cd3131\)[^"]*"[^>]*>base/);
|
||||
assert.match(html, /color:\s*#ff8700[^"]*"[^>]*>cube/);
|
||||
assert.match(html, /color:\s*var\(--term-custom-ff8700, #ff8700\)[^"]*"[^>]*>cube/);
|
||||
});
|
||||
|
||||
test("inverse text falls back to themeable default background/foreground", () => {
|
||||
|
||||
Reference in new issue
Block a user