mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-18 13:43:49 +01:00
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:
parent
b3c0027b66
commit
be4b0267a6
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user