fix(app): terminal rework (#14217)
This commit is contained in:
@@ -320,8 +320,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const mod = loaded.mod
|
const mod = loaded.mod
|
||||||
const g = loaded.ghostty
|
const g = loaded.ghostty
|
||||||
|
|
||||||
const once = { value: false }
|
|
||||||
|
|
||||||
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
||||||
const restoreSize =
|
const restoreSize =
|
||||||
restore &&
|
restore &&
|
||||||
@@ -416,20 +414,28 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const write = (data: string) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
if (!output) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output.push(data)
|
||||||
|
output.flush(resolve)
|
||||||
|
})
|
||||||
|
|
||||||
if (restore && restoreSize) {
|
if (restore && restoreSize) {
|
||||||
t.write(restore, () => {
|
await write(restore)
|
||||||
fit.fit()
|
fit.fit()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
||||||
startResize()
|
startResize()
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
fit.fit()
|
fit.fit()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
if (restore) {
|
if (restore) {
|
||||||
t.write(restore, () => {
|
await write(restore)
|
||||||
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
startResize()
|
startResize()
|
||||||
}
|
}
|
||||||
@@ -438,38 +444,32 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
// console.log("Scroll position:", ydisp)
|
// console.log("Scroll position:", ydisp)
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
const once = { value: false }
|
||||||
|
let closing = false
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
||||||
url.searchParams.set("directory", sdk.directory)
|
url.searchParams.set("directory", sdk.directory)
|
||||||
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||||
url.username = server.current?.http.username ?? ""
|
url.username = server.current?.http.username ?? ""
|
||||||
url.password = server.current?.http.password ?? ""
|
url.password = server.current?.http.password ?? ""
|
||||||
|
|
||||||
const socket = new WebSocket(url)
|
const socket = new WebSocket(url)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
cleanups.push(() => {
|
|
||||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
|
||||||
})
|
|
||||||
if (disposed) {
|
|
||||||
cleanup()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
local.onConnect?.()
|
local.onConnect?.()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
}
|
}
|
||||||
socket.addEventListener("open", handleOpen)
|
socket.addEventListener("open", handleOpen)
|
||||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
|
||||||
|
|
||||||
if (socket.readyState === WebSocket.OPEN) handleOpen()
|
if (socket.readyState === WebSocket.OPEN) handleOpen()
|
||||||
|
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
|
if (closing) return
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
|
||||||
const bytes = new Uint8Array(event.data)
|
const bytes = new Uint8Array(event.data)
|
||||||
if (bytes[0] !== 0) return
|
if (bytes[0] !== 0) return
|
||||||
const json = decoder.decode(bytes.subarray(1))
|
const json = decoder.decode(bytes.subarray(1))
|
||||||
@@ -491,20 +491,20 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cursor += data.length
|
cursor += data.length
|
||||||
}
|
}
|
||||||
socket.addEventListener("message", handleMessage)
|
socket.addEventListener("message", handleMessage)
|
||||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
|
||||||
|
|
||||||
const handleError = (error: Event) => {
|
const handleError = (error: Event) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
|
if (closing) return
|
||||||
if (once.value) return
|
if (once.value) return
|
||||||
once.value = true
|
once.value = true
|
||||||
console.error("WebSocket error:", error)
|
console.error("WebSocket error:", error)
|
||||||
local.onConnectError?.(error)
|
local.onConnectError?.(error)
|
||||||
}
|
}
|
||||||
socket.addEventListener("error", handleError)
|
socket.addEventListener("error", handleError)
|
||||||
cleanups.push(() => socket.removeEventListener("error", handleError))
|
|
||||||
|
|
||||||
const handleClose = (event: CloseEvent) => {
|
const handleClose = (event: CloseEvent) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
|
if (closing) return
|
||||||
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
||||||
// For other codes (network issues, server restart), trigger error handler
|
// For other codes (network issues, server restart), trigger error handler
|
||||||
if (event.code !== 1000) {
|
if (event.code !== 1000) {
|
||||||
@@ -514,7 +514,15 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
socket.addEventListener("close", handleClose)
|
socket.addEventListener("close", handleClose)
|
||||||
cleanups.push(() => socket.removeEventListener("close", handleClose))
|
|
||||||
|
cleanups.push(() => {
|
||||||
|
closing = true
|
||||||
|
socket.removeEventListener("open", handleOpen)
|
||||||
|
socket.removeEventListener("message", handleMessage)
|
||||||
|
socket.removeEventListener("error", handleError)
|
||||||
|
socket.removeEventListener("close", handleClose)
|
||||||
|
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
void run().catch((err) => {
|
void run().catch((err) => {
|
||||||
|
|||||||
@@ -38,9 +38,34 @@ export function TerminalPanel() {
|
|||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
autoCreated: false,
|
autoCreated: false,
|
||||||
|
everOpened: false,
|
||||||
activeDraggable: undefined as string | undefined,
|
activeDraggable: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const rendered = createMemo(() => isDesktop() && (opened() || store.everOpened))
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(open, (isOpen, prev) => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (!store.everOpened) setStore("everOpened", true)
|
||||||
|
const activeId = terminal.active()
|
||||||
|
if (!activeId) return
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
setTimeout(() => focusTerminalById(activeId), 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prev) return
|
||||||
|
const panel = document.getElementById("terminal-panel")
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
if (!panel || !(activeElement instanceof HTMLElement)) return
|
||||||
|
if (!panel.contains(activeElement)) return
|
||||||
|
activeElement.blur()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!opened()) {
|
if (!opened()) {
|
||||||
setStore("autoCreated", false)
|
setStore("autoCreated", false)
|
||||||
@@ -67,7 +92,7 @@ export function TerminalPanel() {
|
|||||||
on(
|
on(
|
||||||
() => terminal.active(),
|
() => terminal.active(),
|
||||||
(activeId) => {
|
(activeId) => {
|
||||||
if (!activeId || !opened()) return
|
if (!activeId || !open()) return
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
}
|
}
|
||||||
@@ -133,14 +158,22 @@ export function TerminalPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={open()}>
|
<Show when={rendered()}>
|
||||||
<div
|
<div
|
||||||
id="terminal-panel"
|
id="terminal-panel"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={language.t("terminal.title")}
|
aria-label={language.t("terminal.title")}
|
||||||
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
classList={{
|
||||||
style={{ height: `${height()}px` }}
|
"relative w-full flex flex-col shrink-0 overflow-hidden": true,
|
||||||
|
"border-t border-border-weak-base": open(),
|
||||||
|
"pointer-events-none": !open(),
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: `${height()}px`,
|
||||||
|
display: open() ? "flex" : "none",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Show when={open()}>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
size={height()}
|
size={height()}
|
||||||
@@ -150,6 +183,7 @@ export function TerminalPanel() {
|
|||||||
onResize={layout.terminal.resize}
|
onResize={layout.terminal.resize}
|
||||||
onCollapse={close}
|
onCollapse={close}
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={terminal.ready()}
|
when={terminal.ready()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -18,27 +18,26 @@ export namespace Pty {
|
|||||||
|
|
||||||
type Socket = {
|
type Socket = {
|
||||||
readyState: number
|
readyState: number
|
||||||
data: object
|
send: (data: string | Uint8Array | ArrayBuffer) => void
|
||||||
send: (data: string | Uint8Array<ArrayBuffer> | ArrayBuffer) => void
|
|
||||||
close: (code?: number, reason?: string) => void
|
close: (code?: number, reason?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bun's ServerWebSocket has a per-connection `.data` object (set during
|
type Subscriber = {
|
||||||
// `server.upgrade`) that changes when the underlying connection is recycled.
|
id: number
|
||||||
// We keep a reference to a stable part of it so output can't leak even when
|
|
||||||
// websocket objects are reused.
|
|
||||||
const token = (ws: Socket) => {
|
|
||||||
const data = ws.data
|
|
||||||
const events = (data as { events?: unknown }).events
|
|
||||||
if (events && typeof events === "object") return events
|
|
||||||
|
|
||||||
const url = (data as { url?: unknown }).url
|
|
||||||
if (url && typeof url === "object") return url
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
const sockets = new WeakMap<object, number>()
|
||||||
|
const owners = new WeakMap<object, string>()
|
||||||
|
let socketCounter = 0
|
||||||
|
|
||||||
|
const tagSocket = (ws: Socket) => {
|
||||||
|
if (!ws || typeof ws !== "object") return
|
||||||
|
const next = (socketCounter = (socketCounter + 1) % Number.MAX_SAFE_INTEGER)
|
||||||
|
sockets.set(ws, next)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket control frame: 0x00 + UTF-8 JSON.
|
||||||
const meta = (cursor: number) => {
|
const meta = (cursor: number) => {
|
||||||
const json = JSON.stringify({ cursor })
|
const json = JSON.stringify({ cursor })
|
||||||
const bytes = encoder.encode(json)
|
const bytes = encoder.encode(json)
|
||||||
@@ -102,7 +101,7 @@ export namespace Pty {
|
|||||||
buffer: string
|
buffer: string
|
||||||
bufferCursor: number
|
bufferCursor: number
|
||||||
cursor: number
|
cursor: number
|
||||||
subscribers: Map<Socket, object>
|
subscribers: Map<Socket, Subscriber>
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = Instance.state(
|
const state = Instance.state(
|
||||||
@@ -185,13 +184,13 @@ export namespace Pty {
|
|||||||
ptyProcess.onData((chunk) => {
|
ptyProcess.onData((chunk) => {
|
||||||
session.cursor += chunk.length
|
session.cursor += chunk.length
|
||||||
|
|
||||||
for (const [ws, data] of session.subscribers) {
|
for (const [ws, sub] of session.subscribers) {
|
||||||
if (ws.readyState !== 1) {
|
if (ws.readyState !== 1) {
|
||||||
session.subscribers.delete(ws)
|
session.subscribers.delete(ws)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token(ws) !== data) {
|
if (typeof ws === "object" && sockets.get(ws) !== sub.id) {
|
||||||
session.subscribers.delete(ws)
|
session.subscribers.delete(ws)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -280,6 +279,25 @@ export namespace Pty {
|
|||||||
}
|
}
|
||||||
log.info("client connected to session", { id })
|
log.info("client connected to session", { id })
|
||||||
|
|
||||||
|
const socketId = tagSocket(ws)
|
||||||
|
if (socketId === undefined) {
|
||||||
|
ws.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = owners.get(ws)
|
||||||
|
if (previous && previous !== id) {
|
||||||
|
state().get(previous)?.subscribers.delete(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
owners.set(ws, id)
|
||||||
|
session.subscribers.set(ws, { id: socketId })
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
session.subscribers.delete(ws)
|
||||||
|
if (owners.get(ws) === id) owners.delete(ws)
|
||||||
|
}
|
||||||
|
|
||||||
const start = session.bufferCursor
|
const start = session.bufferCursor
|
||||||
const end = session.cursor
|
const end = session.cursor
|
||||||
|
|
||||||
@@ -300,6 +318,7 @@ export namespace Pty {
|
|||||||
ws.send(data.slice(i, i + BUFFER_CHUNK))
|
ws.send(data.slice(i, i + BUFFER_CHUNK))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
cleanup()
|
||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -308,23 +327,17 @@ export namespace Pty {
|
|||||||
try {
|
try {
|
||||||
ws.send(meta(end))
|
ws.send(meta(end))
|
||||||
} catch {
|
} catch {
|
||||||
|
cleanup()
|
||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ws.data || typeof ws.data !== "object") {
|
|
||||||
ws.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.subscribers.set(ws, token(ws))
|
|
||||||
return {
|
return {
|
||||||
onMessage: (message: string | ArrayBuffer) => {
|
onMessage: (message: string | ArrayBuffer) => {
|
||||||
session.process.write(String(message))
|
session.process.write(String(message))
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
log.info("client disconnected from session", { id })
|
log.info("client disconnected from session", { id })
|
||||||
session.subscribers.delete(ws)
|
cleanup()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,18 +163,13 @@ export const PtyRoutes = lazy(() =>
|
|||||||
|
|
||||||
type Socket = {
|
type Socket = {
|
||||||
readyState: number
|
readyState: number
|
||||||
data: object
|
send: (data: string | Uint8Array | ArrayBuffer) => void
|
||||||
send: (data: string | Uint8Array<ArrayBuffer> | ArrayBuffer) => void
|
|
||||||
close: (code?: number, reason?: string) => void
|
close: (code?: number, reason?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSocket = (value: unknown): value is Socket => {
|
const isSocket = (value: unknown): value is Socket => {
|
||||||
if (!value || typeof value !== "object") return false
|
if (!value || typeof value !== "object") return false
|
||||||
if (!("readyState" in value)) return false
|
if (!("readyState" in value)) return false
|
||||||
if (!("data" in value)) return false
|
|
||||||
if (!((value as { data?: unknown }).data && typeof (value as { data?: unknown }).data === "object")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
|
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
|
||||||
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
|
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
|
||||||
return typeof (value as { readyState?: unknown }).readyState === "number"
|
return typeof (value as { readyState?: unknown }).readyState === "number"
|
||||||
@@ -182,12 +177,12 @@ export const PtyRoutes = lazy(() =>
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
onOpen(_event, ws) {
|
onOpen(_event, ws) {
|
||||||
const raw = ws.raw
|
const socket = ws.raw
|
||||||
if (!isSocket(raw)) {
|
if (!isSocket(socket)) {
|
||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handler = Pty.connect(id, raw, cursor)
|
handler = Pty.connect(id, socket, cursor)
|
||||||
},
|
},
|
||||||
onMessage(event) {
|
onMessage(event) {
|
||||||
if (typeof event.data !== "string") return
|
if (typeof event.data !== "string") return
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ describe("pty", () => {
|
|||||||
|
|
||||||
const ws = {
|
const ws = {
|
||||||
readyState: 1,
|
readyState: 1,
|
||||||
data: { events: { connection: "a" } },
|
|
||||||
send: (data: unknown) => {
|
send: (data: unknown) => {
|
||||||
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||||
},
|
},
|
||||||
@@ -31,7 +30,6 @@ describe("pty", () => {
|
|||||||
Pty.connect(a.id, ws as any)
|
Pty.connect(a.id, ws as any)
|
||||||
|
|
||||||
// Now "reuse" the same ws object for another connection.
|
// Now "reuse" the same ws object for another connection.
|
||||||
ws.data = { events: { connection: "b" } }
|
|
||||||
ws.send = (data: unknown) => {
|
ws.send = (data: unknown) => {
|
||||||
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||||
}
|
}
|
||||||
@@ -53,48 +51,4 @@ describe("pty", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("does not leak output when Bun recycles websocket objects before re-connect", async () => {
|
|
||||||
await using dir = await tmpdir({ git: true })
|
|
||||||
|
|
||||||
await Instance.provide({
|
|
||||||
directory: dir.path,
|
|
||||||
fn: async () => {
|
|
||||||
const a = await Pty.create({ command: "cat", title: "a" })
|
|
||||||
try {
|
|
||||||
const outA: string[] = []
|
|
||||||
const outB: string[] = []
|
|
||||||
|
|
||||||
const ws = {
|
|
||||||
readyState: 1,
|
|
||||||
data: { events: { connection: "a" } },
|
|
||||||
send: (data: unknown) => {
|
|
||||||
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
|
||||||
},
|
|
||||||
close: () => {
|
|
||||||
// no-op (simulate abrupt drop)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect "a" first.
|
|
||||||
Pty.connect(a.id, ws as any)
|
|
||||||
outA.length = 0
|
|
||||||
|
|
||||||
// Simulate Bun reusing the same websocket object for another connection
|
|
||||||
// before the new onOpen handler has a chance to tag it.
|
|
||||||
ws.data = { events: { connection: "b" } }
|
|
||||||
ws.send = (data: unknown) => {
|
|
||||||
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
|
||||||
}
|
|
||||||
|
|
||||||
Pty.write(a.id, "AAA\n")
|
|
||||||
await Bun.sleep(100)
|
|
||||||
|
|
||||||
expect(outB.join("")).not.toContain("AAA")
|
|
||||||
} finally {
|
|
||||||
await Pty.remove(a.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
diff --git a/dist/ghostty-web.js b/dist/ghostty-web.js
|
|
||||||
index 7c9d64a617bbeb29d757a1acd54686e582868313..2d61098cdb77fa66cbb162897c5590f35cfcf791 100644
|
|
||||||
--- a/dist/ghostty-web.js
|
|
||||||
+++ b/dist/ghostty-web.js
|
|
||||||
@@ -1285,7 +1285,7 @@ const e = class H {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const C = g.getCodepoint();
|
|
||||||
- C === 0 || C < 32 ? B.push(" ") : B.push(String.fromCodePoint(C));
|
|
||||||
+ C === 0 || C < 32 || C > 1114111 || (C >= 55296 && C <= 57343) ? B.push(" ") : B.push(String.fromCodePoint(C));
|
|
||||||
}
|
|
||||||
return B.join("");
|
|
||||||
}
|
|
||||||
@@ -1484,7 +1484,7 @@ class _ {
|
|
||||||
return;
|
|
||||||
let J = "";
|
|
||||||
A.flags & U.ITALIC && (J += "italic "), A.flags & U.BOLD && (J += "bold "), this.ctx.font = `${J}${this.fontSize}px ${this.fontFamily}`, this.ctx.fillStyle = this.rgbToCSS(w, o, i), A.flags & U.FAINT && (this.ctx.globalAlpha = 0.5);
|
|
||||||
- const s = g, F = C + this.metrics.baseline, a = String.fromCodePoint(A.codepoint || 32);
|
|
||||||
+ const s = g, F = C + this.metrics.baseline, a = (A.codepoint === 0 || A.codepoint == null || A.codepoint < 0 || A.codepoint > 1114111 || (A.codepoint >= 55296 && A.codepoint <= 57343)) ? " " : String.fromCodePoint(A.codepoint);
|
|
||||||
if (this.ctx.fillText(a, s, F), A.flags & U.FAINT && (this.ctx.globalAlpha = 1), A.flags & U.UNDERLINE) {
|
|
||||||
const N = C + this.metrics.baseline + 2;
|
|
||||||
this.ctx.strokeStyle = this.ctx.fillStyle, this.ctx.lineWidth = 1, this.ctx.beginPath(), this.ctx.moveTo(g, N), this.ctx.lineTo(g + I, N), this.ctx.stroke();
|
|
||||||
@@ -1730,7 +1730,7 @@ const L = class R {
|
|
||||||
let G = "";
|
|
||||||
for (let J = M; J <= k; J++) {
|
|
||||||
const s = o[J];
|
|
||||||
- if (s && s.codepoint !== 0) {
|
|
||||||
+ if (s && s.codepoint !== 0 && s.codepoint <= 1114111 && !(s.codepoint >= 55296 && s.codepoint <= 57343)) {
|
|
||||||
const F = String.fromCodePoint(s.codepoint);
|
|
||||||
G += F, F.trim() && (i = G.length);
|
|
||||||
} else
|
|
||||||
@@ -1995,7 +1995,7 @@ const L = class R {
|
|
||||||
if (!Q)
|
|
||||||
return null;
|
|
||||||
const g = (w) => {
|
|
||||||
- if (!w || w.codepoint === 0)
|
|
||||||
+ if (!w || w.codepoint === 0 || w.codepoint > 1114111 || (w.codepoint >= 55296 && w.codepoint <= 57343))
|
|
||||||
return !1;
|
|
||||||
const o = String.fromCodePoint(w.codepoint);
|
|
||||||
return /[\w-]/.test(o);
|
|
||||||
Reference in New Issue
Block a user