char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somtest and palette cleanups: one source for the 16-color terminal palette, no change-detector testsb3a4fb7

main
5.8 KiB140 linesraw
1import { TERMINAL_PALETTE as palette } from "@berry/util/palette.ts";
2
3function indexedColor(index: number): string | undefined {
4  if (!Number.isInteger(index) || index < 0 || index > 255) return undefined;
5  if (index < 16) return palette[index];
6  if (index >= 232) {
7    const gray = 8 + (index - 232) * 10;
8    return `rgb(${gray}, ${gray}, ${gray})`;
9  }
10  const value = index - 16;
11  const levels = [0, 95, 135, 175, 215, 255];
12  return `rgb(${levels[Math.floor(value / 36)]}, ${levels[Math.floor(value / 6) % 6]}, ${
13    levels[value % 6]
14  })`;
15}
16
17export type AnsiRun = { text: string; style: Record<string, string> };
18
19/** An incremental SGR decoder that hides terminal controls, even across chunk boundaries. */
20export function ansiParser() {
21  let foreground: string | undefined;
22  let background: string | undefined;
23  let bold = false;
24  let dim = false;
25  let italic = false;
26  let underline = false;
27  let strike = false;
28  let inverse = false;
29  let mode: "text" | "escape" | "csi" | "osc" | "oscEscape" = "text";
30  let parametersText = "";
31  let invalid = false;
32
33  return {
34    write(source: string): AnsiRun[] {
35      const runs: AnsiRun[] = [];
36      let offset = 0;
37      // discard non-SGR controls, including OSC links and a streaming chunk's unfinished escape
38      while (offset < source.length) {
39        if (mode === "text") {
40          const escape = source.indexOf("\x1b", offset);
41          const text = source.slice(offset, escape === -1 ? source.length : escape);
42          if (text) {
43            const style: Record<string, string> = {};
44            const fg = inverse ? background ?? "var(--panel)" : foreground;
45            const bg = inverse ? foreground ?? "var(--text)" : background;
46            if (fg) style.color = fg;
47            if (bg) style["background-color"] = bg;
48            if (bold) style["font-weight"] = "bold";
49            if (dim) style.opacity = "0.5";
50            if (italic) style["font-style"] = "italic";
51            const decoration = [underline ? "underline" : "", strike ? "line-through" : ""].filter(
52              Boolean,
53            );
54            if (decoration.length) style["text-decoration"] = decoration.join(" ");
55            runs.push({ text, style });
56          }
57          if (escape === -1) break;
58          offset = escape + 1;
59          mode = "escape";
60          continue;
61        }
62        const char = source[offset++];
63        if (mode === "escape") {
64          if (char === "[") {
65            mode = "csi";
66            parametersText = "";
67            invalid = false;
68          } else if (char === "]") mode = "osc";
69          else if (char !== "\x1b" && !(char >= " " && char <= "/")) mode = "text";
70        } else if (mode === "osc") {
71          if (char === "\x07") mode = "text";
72          else if (char === "\x1b") mode = "oscEscape";
73        } else if (mode === "oscEscape") {
74          if (char === "\\" || char === "\x07") mode = "text";
75          else if (char !== "\x1b") mode = "osc";
76        } else if (char >= "@" && char <= "~") {
77          mode = "text";
78          if (char !== "m" || invalid || !/^[\d;]*$/.test(parametersText)) continue;
79          const parameters = parametersText.split(";").map(Number);
80          for (let i = 0; i < parameters.length; i++) {
81            const parameter = parameters[i];
82            if (parameter === 0) {
83              foreground = background = undefined;
84              bold = dim = false;
85              italic = underline = false;
86              strike = inverse = false;
87            } else if (parameter === 1) bold = true;
88            else if (parameter === 2) dim = true;
89            else if (parameter === 3) italic = true;
90            else if (parameter === 4) underline = true;
91            else if (parameter === 7) inverse = true;
92            else if (parameter === 9) strike = true;
93            else if (parameter === 22) bold = dim = false;
94            else if (parameter === 23) italic = false;
95            else if (parameter === 24) underline = false;
96            else if (parameter === 27) inverse = false;
97            else if (parameter === 29) strike = false;
98            else if (parameter === 39) foreground = undefined;
99            else if (parameter === 49) background = undefined;
100            else if (parameter >= 30 && parameter <= 37) foreground = palette[parameter - 30];
101            else if (parameter >= 90 && parameter <= 97) foreground = palette[parameter - 82];
102            else if (parameter >= 40 && parameter <= 47) background = palette[parameter - 40];
103            else if (parameter >= 100 && parameter <= 107) background = palette[parameter - 92];
104            else if (parameter === 38 || parameter === 48) {
105              const mode = parameters[++i];
106              let color: string | undefined;
107              if (mode === 5) color = indexedColor(parameters[++i]);
108              else if (mode === 2) {
109                const rgb = parameters.slice(i + 1, i + 4);
110                i += 3;
111                if (
112                  rgb.length === 3 && rgb.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)
113                ) {
114                  color = `rgb(${rgb.join(", ")})`;
115                }
116              } else break;
117              if (color !== undefined) {
118                if (parameter === 38) foreground = color;
119                else background = color;
120              }
121            }
122          }
123          parametersText = "";
124        } else if (char >= " " && char <= "?") {
125          // bound malformed CSI sequences; OSC payloads are never buffered
126          if (parametersText.length < 1024 && char >= "0" && !invalid) parametersText += char;
127          else invalid = true;
128        } else {
129          mode = char === "\x1b" ? "escape" : "text";
130        }
131      }
132      return runs;
133    },
134  };
135}
136
137/** Text and SGR styles, without interpreting terminal commands or output as markup. */
138export function ansiRuns(source: string): AnsiRun[] {
139  return ansiParser().write(source);
140}