TERMINAL: Greatly improve ANSI handling (#485)

- Add support for 2;r;g;b color codes (much easier to deal with
  than 5;x256 style codes)
- Fix 40-47 (standard background colors) so that they work
- Add support for italic
- Add support for empty arguments interpreted as 0
  (0 is still not supported for "reset style", since it's not needed
  with our non-standard usage of resetting styles on every escape
  sequence, and it might cause compat issues)
- Fix ordering of 0-15 in the x256 colors to match the standard. The
  "main" colors (selected via 30-37 for FG and 40-47 for BG) are still
  artificially bright for FG, kept for compatibility, but there's no
  reason to screw up the x256 colors. (Hopefully usage of that section
  should be small anyway.)
This commit is contained in:
David Walker 2023-04-24 08:04:37 -07:00 committed by GitHub
parent b3c0027b66
commit be4b0267a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -114,61 +114,80 @@ function ansiCodeStyle(code: string | null): Record<string, any> {
// and for background colors we use the dark color set. Of course, all colors are available
// via the longer ESC[n8;5;c] sequence (n={3,4}, c=color). Ideally, these 8-bit maps could
// be managed in the user preferences/theme.
const COLOR_MAP_BRIGHT: Record<number, string> = {
0: "#404040",
1: "#ff0000",
2: "#00ff00",
3: "#ffff00",
4: "#0000ff",
5: "#ff00ff",
6: "#00ffff",
7: "#ffffff",
};
const COLOR_MAP_DARK: Record<number, string> = {
8: "#000000",
9: "#800000",
10: "#008000",
11: "#808000",
12: "#000080",
13: "#800080",
14: "#008080",
15: "#c0c0c0",
// Later note: The above justification is a bit suspect, and I doubt that the compatibility break
// vs standard ANSI codes is worth it. But, it's the system that's been baked in to BB for years
// now, so too late to change.
const COLOR_MAP_BRIGHT: string[] = [
"#404040",
"#ff0000",
"#00ff00",
"#ffff00",
"#0000ff",
"#ff00ff",
"#00ffff",
"#ffffff",
];
const COLOR_MAP_DARK: string[] = [
"#000000",
"#800000",
"#008000",
"#808000",
"#000080",
"#800080",
"#008080",
"#c0c0c0",
];
// Returns [parts_consumed, style_string].
// [-1, _] signals an error in parsing.
const ansi2rgb = (codeParts: number[], startIdx: number): [number, string] => {
if (codeParts[startIdx] === 5) {
if (codeParts.length <= startIdx + 1) {
// Don't have enough data, but we have to consume what we've seen so far
return [codeParts.length - startIdx, "inherit"];
}
const code = codeParts[startIdx + 1];
/* eslint-disable yoda */
if (0 <= code && code < 8) {
// x8 RGB
return [2, COLOR_MAP_DARK[code]];
}
if (8 <= code && code < 16) {
// x8 RGB - "High Intensity"
return [2, COLOR_MAP_BRIGHT[code - 8]];
}
if (16 <= code && code < 232) {
// x216 RGB
const base = code - 16;
const ir = Math.floor(base / 36);
const ig = Math.floor((base % 36) / 6);
const ib = Math.floor((base % 6) / 1);
const r = ir <= 0 ? 0 : 55 + ir * 40;
const g = ig <= 0 ? 0 : 55 + ig * 40;
const b = ib <= 0 ? 0 : 55 + ib * 40;
return [2, `rgb(${r}, ${g}, ${b})`];
}
if (232 <= code && code < 256) {
// x32 greyscale
const base = code - 232;
const grey = base * 10 + 8;
return [2, `rgb(${grey}, ${grey}, ${grey})`];
}
// Value out of range, but the escape sequence is still well-formed
return [2, "inherit"];
} else if (codeParts[startIdx] === 2) {
if (codeParts.length <= startIdx + 3) {
// Don't have enough data, but we have to consume what we've seen so far
return [codeParts.length - startIdx, "inherit"];
}
return [4, `rgb(${codeParts[startIdx + 1]}, ${codeParts[startIdx + 2]}, ${codeParts[startIdx + 3]})`];
}
return [-1, ""];
};
const ansi2rgb = (code: number): string => {
/* eslint-disable yoda */
if (0 <= code && code < 8) {
// x8 RGB
return COLOR_MAP_BRIGHT[code];
}
if (8 <= code && code < 16) {
// x8 RGB - "High Intensity" (but here, actually the dark set)
return COLOR_MAP_DARK[code];
}
if (16 <= code && code < 232) {
// x216 RGB
const base = code - 16;
const ir = Math.floor(base / 36);
const ig = Math.floor((base % 36) / 6);
const ib = Math.floor((base % 6) / 1);
const r = ir <= 0 ? 0 : 55 + ir * 40;
const g = ig <= 0 ? 0 : 55 + ig * 40;
const b = ib <= 0 ? 0 : 55 + ib * 40;
return `rgb(${r}, ${g}, ${b})`;
}
if (232 <= code && code < 256) {
// x32 greyscale
const base = code - 232;
const grey = base * 10 + 8;
return `rgb(${grey}, ${grey}, ${grey})`;
}
// shouldn't get here (under normal circumstances), but just in case
return "initial";
};
type styleKey = "fontWeight" | "textDecoration" | "color" | "backgroundColor" | "padding";
const style: {
fontWeight?: string;
fontStyle?: string;
textDecoration?: string;
color?: string;
backgroundColor?: string;
@ -179,50 +198,37 @@ function ansiCodeStyle(code: string | null): Record<string, any> {
return style;
}
const codeParts = code
.split(";")
.map((p) => parseInt(p))
.filter(
(p, i, arr) =>
// If the sequence is 38;5 (x256 foreground color) or 48;5 (x256 background color),
// filter out the 5 so the next codePart after {38,48} is the color code.
p != 5 || i == 0 || (arr[i - 1] != 38 && arr[i - 1] != 48),
);
const codeParts = code.split(";").map((p) => (p === "" ? 0 : parseInt(p)));
let nextStyleKey: styleKey | null = null;
codeParts.forEach((codePart) => {
/* eslint-disable yoda */
if (nextStyleKey !== null) {
style[nextStyleKey] = ansi2rgb(codePart);
nextStyleKey = null;
}
for (let i = 0; i < codeParts.length; ++i) {
const codePart = codeParts[i];
// Decorations
else if (codePart == 1) {
if (codePart === 1) {
style.fontWeight = "bold";
} else if (codePart == 4) {
} else if (codePart === 3) {
style.fontStyle = "italic";
} else if (codePart === 4) {
style.textDecoration = "underline";
}
/* eslint-disable yoda */
// Foreground Color (x8)
else if (30 <= codePart && codePart < 38) {
if (COLOR_MAP_BRIGHT[codePart % 10]) {
style.color = COLOR_MAP_BRIGHT[codePart % 10];
}
style.color = COLOR_MAP_BRIGHT[codePart - 30];
}
// Background Color (x8)
else if (40 <= codePart && codePart < 48) {
if (COLOR_MAP_DARK[codePart % 10]) {
style.backgroundColor = COLOR_MAP_DARK[codePart % 10];
}
style.backgroundColor = COLOR_MAP_DARK[codePart - 40];
}
// Foreground Color (x256)
else if (codePart == 38) {
nextStyleKey = "color";
else if (codePart === 38 || codePart === 48) {
const [extra, colorString] = ansi2rgb(codeParts, i + 1);
// If it was an invalid code, we consume no extra parts
if (extra > 0) {
i += extra;
style[codePart === 38 ? "color" : "backgroundColor"] = colorString;
}
}
// Background Color (x256)
else if (codePart == 48) {
nextStyleKey = "backgroundColor";
}
});
}
// If a background color is set, add slight padding to increase the background fill area.
// This was previously display:inline-block, but that has display errors when line breaks are used.
if (style.backgroundColor) {