| author | |
| committer | |
| log | c3e37094e04de71accf497c6110c5305223e0090 |
| tree | 7cd85e3afb158622e932bb5446501d713b1c3ec3 |
| parent | 9b5d416872591d215c4a16d6f87798c8486522d8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>2 files changed, 58 insertions(+), 5 deletions(-)
apps/web/src/terminal/ghostty/core.test.ts created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { ghosttyCellText } from "./core"; | |
| 4 | ||
| 5 | function codepointView(codepoints: ReadonlyArray<number>): DataView { | |
| 6 | const view = new DataView(new ArrayBuffer(codepoints.length * 4)); | |
| 7 | codepoints.forEach((codepoint, index) => view.setUint32(index * 4, codepoint, true)); | |
| 8 | return view; | |
| 9 | } | |
| 10 | ||
| 11 | describe("ghosttyCellText", () => { | |
| 12 | it("converts oversized grapheme clusters without hitting engine spread limits", () => { | |
| 13 | // A program printing one base character followed by a huge run of | |
| 14 | // combining marks packs the whole cluster into one cell; spreading that | |
| 15 | // many arguments into String.fromCodePoint once overflows the call stack. | |
| 16 | const graphemeLength = 130_000; | |
| 17 | const view = new DataView(new ArrayBuffer(graphemeLength * 4)); | |
| 18 | for (let index = 0; index < graphemeLength; index += 1) { | |
| 19 | view.setUint32(index * 4, index === 0 ? "a".codePointAt(0)! : 0x301, true); | |
| 20 | } | |
| 21 | const text = ghosttyCellText(view, graphemeLength); | |
| 22 | expect(text.length).toBe(graphemeLength); | |
| 23 | expect(text.codePointAt(0)).toBe("a".codePointAt(0)); | |
| 24 | expect(text.codePointAt(1)).toBe(0x301); | |
| 25 | expect(text.codePointAt(graphemeLength - 1)).toBe(0x301); | |
| 26 | }); | |
| 27 | ||
| 28 | it("converts small clusters including astral codepoints", () => { | |
| 29 | const text = ghosttyCellText(codepointView([0x1f642, 0x20e3]), 2); | |
| 30 | expect([...text]).toEqual(["\u{1F642}", "\u{20E3}"]); | |
| 31 | }); | |
| 32 | ||
| 33 | it("returns an empty string for empty cells", () => { | |
| 34 | expect(ghosttyCellText(codepointView([]), 0)).toBe(""); | |
| 35 | }); | |
| 36 | }); |
apps/web/src/terminal/ghostty/core.ts+22-5| ... | ... | @@ -166,6 +166,27 @@ function sameColor(left: GhosttyColor, right: GhosttyColor): boolean { |
| 166 | 166 | return left.r === right.r && left.g === right.g && left.b === right.b; |
| 167 | 167 | } |
| 168 | 168 | |
| 169 | /** | |
| 170 | * A terminal program can print one base character followed by a huge run of | |
| 171 | * combining marks, packing hundreds of thousands of codepoints into a single | |
| 172 | * cell that still fits the scrollback buffer. Engines cap spread-call | |
| 173 | * arguments far below that, so convert in bounded chunks instead of spreading | |
| 174 | * every codepoint into String.fromCodePoint at once. | |
| 175 | */ | |
| 176 | export function ghosttyCellText(codepointView: DataView, graphemeLength: number): string { | |
| 177 | const CHUNK_SIZE = 4_096; | |
| 178 | let text = ""; | |
| 179 | for (let start = 0; start < graphemeLength; start += CHUNK_SIZE) { | |
| 180 | const count = Math.min(CHUNK_SIZE, graphemeLength - start); | |
| 181 | const codes = new Array<number>(count); | |
| 182 | for (let index = 0; index < count; index += 1) { | |
| 183 | codes[index] = codepointView.getUint32((start + index) * 4, true); | |
| 184 | } | |
| 185 | text += String.fromCodePoint(...codes); | |
| 186 | } | |
| 187 | return text; | |
| 188 | } | |
| 189 | ||
| 169 | 190 | export class GhosttyTerminalCore { |
| 170 | 191 | private readonly runtime: GhosttyRuntime; |
| 171 | 192 | private terminalSlot = 0; |
| ... | ... | @@ -960,11 +981,7 @@ export class GhosttyTerminalCore { |
| 960 | 981 | // Read through a DataView: the byte-array allocator guarantees no |
| 961 | 982 | // 4-byte alignment, which a Uint32Array view would require. |
| 962 | 983 | const codepointView = this.runtime.view(codepoints, bufferSize); |
| 963 | const codes: number[] = []; | |
| 964 | for (let index = 0; index < graphemeLength; index += 1) { | |
| 965 | codes.push(codepointView.getUint32(index * 4, true)); | |
| 966 | } | |
| 967 | text = String.fromCodePoint(...codes); | |
| 984 | text = ghosttyCellText(codepointView, graphemeLength); | |
| 968 | 985 | } |
| 969 | 986 | this.runtime.free(codepoints, bufferSize); |
| 970 | 987 | } |