summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/desktop/src')
-rw-r--r--packages/desktop/src/addons/serialize.test.ts272
-rw-r--r--packages/desktop/src/addons/serialize.ts201
-rw-r--r--packages/desktop/src/components/terminal.tsx17
3 files changed, 361 insertions, 129 deletions
diff --git a/packages/desktop/src/addons/serialize.test.ts b/packages/desktop/src/addons/serialize.test.ts
new file mode 100644
index 000000000..ad165f43f
--- /dev/null
+++ b/packages/desktop/src/addons/serialize.test.ts
@@ -0,0 +1,272 @@
+import { describe, test, expect, beforeAll, afterEach } from "bun:test"
+import { Terminal, Ghostty } from "ghostty-web"
+import { SerializeAddon } from "./serialize"
+
+let ghostty: Ghostty
+beforeAll(async () => {
+ ghostty = await Ghostty.load()
+})
+
+const terminals: Terminal[] = []
+
+afterEach(() => {
+ for (const term of terminals) {
+ term.dispose()
+ }
+ terminals.length = 0
+ document.body.innerHTML = ""
+})
+
+function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } {
+ const container = document.createElement("div")
+ document.body.appendChild(container)
+
+ const term = new Terminal({ cols, rows, ghostty })
+ const addon = new SerializeAddon()
+ term.loadAddon(addon)
+ term.open(container)
+ terminals.push(term)
+
+ return { term, addon, container }
+}
+
+function writeAndWait(term: Terminal, data: string): Promise<void> {
+ return new Promise((resolve) => {
+ term.write(data, resolve)
+ })
+}
+
+describe("SerializeAddon", () => {
+ describe("ANSI color preservation", () => {
+ test("should preserve text attributes (bold, italic, underline)", async () => {
+ const { term, addon } = createTerminal()
+
+ const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m"
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ expect(origLine!.getCell(0)!.isBold()).toBe(1)
+ expect(origLine!.getCell(5)!.isItalic()).toBe(1)
+ expect(origLine!.getCell(12)!.isUnderline()).toBe(1)
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const line = term2.buffer.active.getLine(0)
+
+ const boldCell = line!.getCell(0)
+ expect(boldCell!.getChars()).toBe("B")
+ expect(boldCell!.isBold()).toBe(1)
+
+ const italicCell = line!.getCell(5)
+ expect(italicCell!.getChars()).toBe("I")
+ expect(italicCell!.isItalic()).toBe(1)
+
+ const underCell = line!.getCell(12)
+ expect(underCell!.getChars()).toBe("U")
+ expect(underCell!.isUnderline()).toBe(1)
+ })
+
+ test("should preserve basic 16-color foreground colors", async () => {
+ const { term, addon } = createTerminal()
+
+ const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL"
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origRedFg = origLine!.getCell(0)!.getFgColor()
+ const origGreenFg = origLine!.getCell(3)!.getFgColor()
+ const origBlueFg = origLine!.getCell(8)!.getFgColor()
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const line = term2.buffer.active.getLine(0)
+ expect(line).toBeDefined()
+
+ const redCell = line!.getCell(0)
+ expect(redCell!.getChars()).toBe("R")
+ expect(redCell!.getFgColor()).toBe(origRedFg)
+
+ const greenCell = line!.getCell(3)
+ expect(greenCell!.getChars()).toBe("G")
+ expect(greenCell!.getFgColor()).toBe(origGreenFg)
+
+ const blueCell = line!.getCell(8)
+ expect(blueCell!.getChars()).toBe("B")
+ expect(blueCell!.getFgColor()).toBe(origBlueFg)
+ })
+
+ test("should preserve 256-color palette colors", async () => {
+ const { term, addon } = createTerminal()
+
+ const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL"
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origRedFg = origLine!.getCell(0)!.getFgColor()
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const line = term2.buffer.active.getLine(0)
+ const redCell = line!.getCell(0)
+ expect(redCell!.getChars()).toBe("R")
+ expect(redCell!.getFgColor()).toBe(origRedFg)
+ })
+
+ test("should preserve RGB/truecolor colors", async () => {
+ const { term, addon } = createTerminal()
+
+ const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL"
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origRgbFg = origLine!.getCell(0)!.getFgColor()
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const line = term2.buffer.active.getLine(0)
+ const rgbCell = line!.getCell(0)
+ expect(rgbCell!.getChars()).toBe("R")
+ expect(rgbCell!.getFgColor()).toBe(origRgbFg)
+ })
+
+ test("should preserve background colors", async () => {
+ const { term, addon } = createTerminal()
+
+ const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL"
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origRedBg = origLine!.getCell(0)!.getBgColor()
+ const origGreenBg = origLine!.getCell(6)!.getBgColor()
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const line = term2.buffer.active.getLine(0)
+
+ const redBgCell = line!.getCell(0)
+ expect(redBgCell!.getChars()).toBe("R")
+ expect(redBgCell!.getBgColor()).toBe(origRedBg)
+
+ const greenBgCell = line!.getCell(6)
+ expect(greenBgCell!.getChars()).toBe("G")
+ expect(greenBgCell!.getBgColor()).toBe(origGreenBg)
+ })
+
+ test("should handle combined colors and attributes", async () => {
+ const { term, addon } = createTerminal()
+
+ const input =
+ "\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL "
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origFg = origLine!.getCell(0)!.getFgColor()
+ const origBg = origLine!.getCell(0)!.getBgColor()
+ expect(origLine!.getCell(0)!.isBold()).toBe(1)
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+ const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "")
+
+ expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true)
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, cleanSerialized)
+
+ const line = term2.buffer.active.getLine(0)
+ const comboCell = line!.getCell(0)
+
+ expect(comboCell!.getChars()).toBe("C")
+ expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m")
+ })
+ })
+
+ describe("round-trip serialization", () => {
+ test("should not produce ECH sequences", async () => {
+ const { term, addon } = createTerminal()
+
+ await writeAndWait(term, "\x1b[31mHello\x1b[0m World")
+
+ const serialized = addon.serialize()
+
+ const hasECH = /\x1b\[\d+X/.test(serialized)
+ expect(hasECH).toBe(false)
+ })
+
+ test("multi-line content should not have garbage characters", async () => {
+ const { term, addon } = createTerminal()
+
+ const content = [
+ "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
+ "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
+ "total 42",
+ ].join("\r\n")
+
+ await writeAndWait(term, content)
+
+ const serialized = addon.serialize()
+
+ expect(/\x1b\[\d+X/.test(serialized)).toBe(false)
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ for (let row = 0; row < 3; row++) {
+ const line = term2.buffer.active.getLine(row)?.translateToString(true)
+ expect(line?.includes("𑼝")).toBe(false)
+ }
+
+ expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
+ expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
+ expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
+ })
+
+ test("serialized output written to new terminal should match original colors", async () => {
+ const { term, addon } = createTerminal(40, 5)
+
+ const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! "
+ await writeAndWait(term, input)
+
+ const origLine = term.buffer.active.getLine(0)
+ const origHelloFg = origLine!.getCell(0)!.getFgColor()
+ const origWorldFg = origLine!.getCell(6)!.getFgColor()
+
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+
+ const { term: term2 } = createTerminal(40, 5)
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ const newLine = term2.buffer.active.getLine(0)
+
+ expect(newLine!.getCell(0)!.getChars()).toBe("H")
+ expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg)
+
+ expect(newLine!.getCell(6)!.getChars()).toBe("W")
+ expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg)
+
+ expect(newLine!.getCell(11)!.getChars()).toBe("!")
+ })
+ })
+})
diff --git a/packages/desktop/src/addons/serialize.ts b/packages/desktop/src/addons/serialize.ts
index c9f848180..cb1ff8442 100644
--- a/packages/desktop/src/addons/serialize.ts
+++ b/packages/desktop/src/addons/serialize.ts
@@ -157,6 +157,23 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
abstract class BaseSerializeHandler {
constructor(protected readonly _buffer: IBuffer) {}
+ private _isRealContent(codepoint: number): boolean {
+ if (codepoint === 0) return false
+ if (codepoint >= 0xf000) return false
+ return true
+ }
+
+ private _findLastContentColumn(line: IBufferLine): number {
+ let lastContent = -1
+ for (let col = 0; col < line.length; col++) {
+ const cell = line.getCell(col)
+ if (cell && this._isRealContent(cell.getCode())) {
+ lastContent = col
+ }
+ }
+ return lastContent + 1
+ }
+
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
let oldCell = this._buffer.getNullCell()
@@ -171,7 +188,8 @@ abstract class BaseSerializeHandler {
const line = this._buffer.getLine(row)
if (line) {
const startLineColumn = row === range.start.y ? startColumn : 0
- const endLineColumn = row === range.end.y ? endColumn : line.length
+ const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
+ const endLineColumn = Math.min(maxColumn, line.length)
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col)
if (!c) {
@@ -209,17 +227,11 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _currentRow: string = ""
private _nullCellCount: number = 0
private _cursorStyle: IBufferCell
- private _cursorStyleRow: number = 0
- private _cursorStyleCol: number = 0
- private _backgroundCell: IBufferCell
private _firstRow: number = 0
private _lastCursorRow: number = 0
private _lastCursorCol: number = 0
private _lastContentCursorRow: number = 0
private _lastContentCursorCol: number = 0
- private _thisRowLastChar: IBufferCell
- private _thisRowLastSecondChar: IBufferCell
- private _nextRowFirstChar: IBufferCell
constructor(
buffer: IBuffer,
@@ -227,10 +239,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
) {
super(buffer)
this._cursorStyle = this._buffer.getNullCell()
- this._backgroundCell = this._buffer.getNullCell()
- this._thisRowLastChar = this._buffer.getNullCell()
- this._thisRowLastSecondChar = this._buffer.getNullCell()
- this._nextRowFirstChar = this._buffer.getNullCell()
}
protected _beforeSerialize(rows: number, start: number, _end: number): void {
@@ -241,82 +249,15 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
protected _rowEnd(row: number, isLastRow: boolean): void {
- // if there is colorful empty cell at line end, we must pad it back
- if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
- this._currentRow += `\u001b[${this._nullCellCount}X`
- }
-
let rowSeparator = ""
if (!isLastRow) {
- // Enable BCE
- if (row - this._firstRow >= this._terminal.rows) {
- const line = this._buffer.getLine(this._cursorStyleRow)
- const cell = line?.getCell(this._cursorStyleCol)
- if (cell) {
- this._backgroundCell = cell
- }
- }
+ const nextLine = this._buffer.getLine(row + 1)
- const currentLine = this._buffer.getLine(row)!
- const nextLine = this._buffer.getLine(row + 1)!
-
- if (!nextLine.isWrapped) {
+ if (!nextLine?.isWrapped) {
rowSeparator = "\r\n"
this._lastCursorRow = row + 1
this._lastCursorCol = 0
- } else {
- rowSeparator = ""
- const thisRowLastChar = currentLine.getCell(currentLine.length - 1)
- const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2)
- const nextRowFirstChar = nextLine.getCell(0)
-
- if (thisRowLastChar) this._thisRowLastChar = thisRowLastChar
- if (thisRowLastSecondChar) this._thisRowLastSecondChar = thisRowLastSecondChar
- if (nextRowFirstChar) this._nextRowFirstChar = nextRowFirstChar
-
- const isNextRowFirstCharDoubleWidth = this._nextRowFirstChar.getWidth() > 1
-
- let isValid = false
-
- if (
- this._nextRowFirstChar.getChars() &&
- (isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0)
- ) {
- if (
- (this._thisRowLastChar.getChars() || this._thisRowLastChar.getWidth() === 0) &&
- equalBg(this._thisRowLastChar, this._nextRowFirstChar)
- ) {
- isValid = true
- }
-
- if (
- isNextRowFirstCharDoubleWidth &&
- (this._thisRowLastSecondChar.getChars() || this._thisRowLastSecondChar.getWidth() === 0) &&
- equalBg(this._thisRowLastChar, this._nextRowFirstChar) &&
- equalBg(this._thisRowLastSecondChar, this._nextRowFirstChar)
- ) {
- isValid = true
- }
- }
-
- if (!isValid) {
- rowSeparator = "-".repeat(this._nullCellCount + 1)
- rowSeparator += "\u001b[1D\u001b[1X"
-
- if (this._nullCellCount > 0) {
- rowSeparator += "\u001b[A"
- rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`
- rowSeparator += `\u001b[${this._nullCellCount}X`
- rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`
- rowSeparator += "\u001b[B"
- }
-
- this._lastContentCursorRow = row + 1
- this._lastContentCursorCol = 0
- this._lastCursorRow = row + 1
- this._lastCursorCol = 0
- }
}
}
@@ -338,11 +279,36 @@ class StringSerializeHandler extends BaseSerializeHandler {
sgrSeq.push(0)
}
} else {
+ if (flagsChanged) {
+ if (!!cell.isInverse() !== !!oldCell.isInverse()) {
+ sgrSeq.push(cell.isInverse() ? 7 : 27)
+ }
+ if (!!cell.isBold() !== !!oldCell.isBold()) {
+ sgrSeq.push(cell.isBold() ? 1 : 22)
+ }
+ if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
+ sgrSeq.push(cell.isUnderline() ? 4 : 24)
+ }
+ if (!!cell.isBlink() !== !!oldCell.isBlink()) {
+ sgrSeq.push(cell.isBlink() ? 5 : 25)
+ }
+ if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
+ sgrSeq.push(cell.isInvisible() ? 8 : 28)
+ }
+ if (!!cell.isItalic() !== !!oldCell.isItalic()) {
+ sgrSeq.push(cell.isItalic() ? 3 : 23)
+ }
+ if (!!cell.isDim() !== !!oldCell.isDim()) {
+ sgrSeq.push(cell.isDim() ? 2 : 22)
+ }
+ if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
+ sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
+ }
+ }
if (fgChanged) {
const color = cell.getFgColor()
const mode = cell.getFgColorMode()
- if (mode === 2) {
- // RGB
+ if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
@@ -358,8 +324,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (bgChanged) {
const color = cell.getBgColor()
const mode = cell.getBgColorMode()
- if (mode === 2) {
- // RGB
+ if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
@@ -372,32 +337,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
sgrSeq.push(49)
}
}
- if (flagsChanged) {
- if (!!cell.isInverse() !== !!oldCell.isInverse()) {
- sgrSeq.push(cell.isInverse() ? 7 : 27)
- }
- if (!!cell.isBold() !== !!oldCell.isBold()) {
- sgrSeq.push(cell.isBold() ? 1 : 22)
- }
- if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
- sgrSeq.push(cell.isUnderline() ? 4 : 24)
- }
- if (!!cell.isBlink() !== !!oldCell.isBlink()) {
- sgrSeq.push(cell.isBlink() ? 5 : 25)
- }
- if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
- sgrSeq.push(cell.isInvisible() ? 8 : 28)
- }
- if (!!cell.isItalic() !== !!oldCell.isItalic()) {
- sgrSeq.push(cell.isItalic() ? 3 : 23)
- }
- if (!!cell.isDim() !== !!oldCell.isDim()) {
- sgrSeq.push(cell.isDim() ? 2 : 22)
- }
- if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
- sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
- }
- }
}
}
@@ -405,9 +344,31 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
private _isAttributeDefault(cell: IBufferCell): boolean {
+ const mode = cell.getFgColorMode()
+ const bgMode = cell.getBgColorMode()
+
+ if (mode === 0 && bgMode === 0) {
+ return (
+ !cell.isBold() &&
+ !cell.isItalic() &&
+ !cell.isUnderline() &&
+ !cell.isBlink() &&
+ !cell.isInverse() &&
+ !cell.isInvisible() &&
+ !cell.isDim() &&
+ !cell.isStrikethrough()
+ )
+ }
+
+ const fgColor = cell.getFgColor()
+ const bgColor = cell.getBgColor()
+ const nullCell = this._buffer.getNullCell()
+ const nullFg = nullCell.getFgColor()
+ const nullBg = nullCell.getBgColor()
+
return (
- cell.getFgColorMode() === 0 &&
- cell.getBgColorMode() === 0 &&
+ fgColor === nullFg &&
+ bgColor === nullBg &&
!cell.isBold() &&
!cell.isItalic() &&
!cell.isUnderline() &&
@@ -426,7 +387,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
return
}
- const isEmptyCell = cell.getChars() === ""
+ const codepoint = cell.getCode()
+ const isGarbage = codepoint >= 0xf000
+ const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
@@ -434,9 +397,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (styleChanged) {
if (this._nullCellCount > 0) {
- if (!equalBg(this._cursorStyle, this._backgroundCell)) {
- this._currentRow += `\u001b[${this._nullCellCount}X`
- }
this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
@@ -450,8 +410,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
const cellFromLine = line?.getCell(col)
if (cellFromLine) {
this._cursorStyle = cellFromLine
- this._cursorStyleRow = row
- this._cursorStyleCol = col
}
}
@@ -459,12 +417,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
this._nullCellCount += cell.getWidth()
} else {
if (this._nullCellCount > 0) {
- if (equalBg(this._cursorStyle, this._backgroundCell)) {
- this._currentRow += `\u001b[${this._nullCellCount}C`
- } else {
- this._currentRow += `\u001b[${this._nullCellCount}X`
- this._currentRow += `\u001b[${this._nullCellCount}C`
- }
+ this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx
index aaf5da880..b312c3589 100644
--- a/packages/desktop/src/components/terminal.tsx
+++ b/packages/desktop/src/components/terminal.tsx
@@ -1,11 +1,9 @@
-import { init, Terminal as Term, FitAddon } from "ghostty-web"
+import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
-await init()
-
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
@@ -19,10 +17,14 @@ export const Terminal = (props: TerminalProps) => {
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
let term: Term
+ let ghostty: Ghostty
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
+ let handleResize: () => void
onMount(async () => {
+ ghostty = await Ghostty.load()
+
ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
term = new Term({
cursorBlink: true,
@@ -34,6 +36,7 @@ export const Terminal = (props: TerminalProps) => {
foreground: "#d4d4d4",
},
scrollback: 10_000,
+ ghostty,
})
term.attachCustomKeyEventHandler((event) => {
// allow for ctrl-` to toggle terminal in parent
@@ -60,13 +63,14 @@ export const Terminal = (props: TerminalProps) => {
if (local.pty.scrollY) {
term.scrollToLine(local.pty.scrollY)
}
+ fitAddon.fit()
}
container.focus()
- fitAddon.fit()
fitAddon.observeResize()
- window.addEventListener("resize", () => fitAddon.fit())
+ handleResize = () => fitAddon.fit()
+ window.addEventListener("resize", handleResize)
term.onResize(async (size) => {
if (ws && ws.readyState === WebSocket.OPEN) {
await sdk.client.pty.update({
@@ -118,6 +122,9 @@ export const Terminal = (props: TerminalProps) => {
})
onCleanup(() => {
+ if (handleResize) {
+ window.removeEventListener("resize", handleResize)
+ }
if (serializeAddon && props.onCleanup) {
const buffer = serializeAddon.serialize()
props.onCleanup({