Skip to content

Commit 1c71604

Browse files
committed
fix(app): terminal resize
1 parent e242fe1 commit 1c71604

1 file changed

Lines changed: 98 additions & 40 deletions

File tree

packages/app/src/components/terminal.tsx

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => {
156156
let serializeAddon: SerializeAddon
157157
let fitAddon: FitAddon
158158
let handleResize: () => void
159+
let fitFrame: number | undefined
160+
let sizeTimer: ReturnType<typeof setTimeout> | undefined
161+
let pendingSize: { cols: number; rows: number } | undefined
162+
let lastSize: { cols: number; rows: number } | undefined
159163
let disposed = false
160164
const cleanups: VoidFunction[] = []
161165
const start =
@@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => {
209213

210214
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
211215

216+
const scheduleFit = () => {
217+
if (disposed) return
218+
if (!fitAddon) return
219+
if (fitFrame !== undefined) return
220+
221+
fitFrame = requestAnimationFrame(() => {
222+
fitFrame = undefined
223+
if (disposed) return
224+
fitAddon.fit()
225+
})
226+
}
227+
228+
const scheduleSize = (cols: number, rows: number) => {
229+
if (disposed) return
230+
if (lastSize?.cols === cols && lastSize?.rows === rows) return
231+
232+
pendingSize = { cols, rows }
233+
234+
if (!lastSize) {
235+
lastSize = pendingSize
236+
void pushSize(cols, rows)
237+
return
238+
}
239+
240+
if (sizeTimer !== undefined) return
241+
sizeTimer = setTimeout(() => {
242+
sizeTimer = undefined
243+
const next = pendingSize
244+
if (!next) return
245+
pendingSize = undefined
246+
if (disposed) return
247+
if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return
248+
lastSize = next
249+
void pushSize(next.cols, next.rows)
250+
}, 100)
251+
}
252+
212253
createEffect(() => {
213254
const colors = getTerminalColors()
214255
setTerminalColors(colors)
@@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => {
220261
const font = monoFontFamily(settings.appearance.font())
221262
if (!term) return
222263
setOptionIfSupported(term, "fontFamily", font)
264+
scheduleFit()
265+
})
266+
267+
let zoom = platform.webviewZoom?.()
268+
createEffect(() => {
269+
const next = platform.webviewZoom?.()
270+
if (next === undefined) return
271+
if (next === zoom) return
272+
zoom = next
273+
scheduleFit()
223274
})
224275

225276
const focusTerminal = () => {
@@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => {
263314

264315
const once = { value: false }
265316

266-
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
267-
url.searchParams.set("directory", sdk.directory)
268-
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
269-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
270-
if (window.__OPENCODE__?.serverPassword) {
271-
url.username = "opencode"
272-
url.password = window.__OPENCODE__?.serverPassword
273-
}
274-
const socket = new WebSocket(url)
275-
socket.binaryType = "arraybuffer"
276-
cleanups.push(() => {
277-
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
278-
})
279-
if (disposed) {
280-
cleanup()
281-
return
282-
}
283-
ws = socket
284-
285317
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
286318
const restoreSize =
287319
restore &&
@@ -344,21 +376,42 @@ export const Terminal = (props: TerminalProps) => {
344376

345377
focusTerminal()
346378

379+
if (typeof document !== "undefined" && document.fonts) {
380+
document.fonts.ready.then(scheduleFit)
381+
}
382+
383+
const onResize = t.onResize((size) => {
384+
scheduleSize(size.cols, size.rows)
385+
})
386+
cleanups.push(() => disposeIfDisposable(onResize))
387+
const onData = t.onData((data) => {
388+
if (ws?.readyState === WebSocket.OPEN) ws.send(data)
389+
})
390+
cleanups.push(() => disposeIfDisposable(onData))
391+
const onKey = t.onKey((key) => {
392+
if (key.key == "Enter") {
393+
props.onSubmit?.()
394+
}
395+
})
396+
cleanups.push(() => disposeIfDisposable(onKey))
397+
347398
const startResize = () => {
348399
fit.observeResize()
349-
handleResize = () => fit.fit()
400+
handleResize = scheduleFit
350401
window.addEventListener("resize", handleResize)
351402
cleanups.push(() => window.removeEventListener("resize", handleResize))
352403
}
353404

354405
if (restore && restoreSize) {
355406
t.write(restore, () => {
356407
fit.fit()
408+
scheduleSize(t.cols, t.rows)
357409
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
358410
startResize()
359411
})
360412
} else {
361413
fit.fit()
414+
scheduleSize(t.cols, t.rows)
362415
if (restore) {
363416
t.write(restore, () => {
364417
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
@@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => {
367420
startResize()
368421
}
369422

370-
const onResize = t.onResize(async (size) => {
371-
if (socket.readyState === WebSocket.OPEN) {
372-
await pushSize(size.cols, size.rows)
373-
}
374-
})
375-
cleanups.push(() => disposeIfDisposable(onResize))
376-
const onData = t.onData((data) => {
377-
if (socket.readyState === WebSocket.OPEN) {
378-
socket.send(data)
379-
}
380-
})
381-
cleanups.push(() => disposeIfDisposable(onData))
382-
const onKey = t.onKey((key) => {
383-
if (key.key == "Enter") {
384-
props.onSubmit?.()
385-
}
386-
})
387-
cleanups.push(() => disposeIfDisposable(onKey))
388423
// t.onScroll((ydisp) => {
389424
// console.log("Scroll position:", ydisp)
390425
// })
391426

427+
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
428+
url.searchParams.set("directory", sdk.directory)
429+
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
430+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
431+
if (window.__OPENCODE__?.serverPassword) {
432+
url.username = "opencode"
433+
url.password = window.__OPENCODE__?.serverPassword
434+
}
435+
const socket = new WebSocket(url)
436+
socket.binaryType = "arraybuffer"
437+
ws = socket
438+
cleanups.push(() => {
439+
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
440+
})
441+
if (disposed) {
442+
cleanup()
443+
return
444+
}
445+
392446
const handleOpen = () => {
393447
local.onConnect?.()
394-
void pushSize(t.cols, t.rows)
448+
scheduleSize(t.cols, t.rows)
395449
}
396450
socket.addEventListener("open", handleOpen)
397451
cleanups.push(() => socket.removeEventListener("open", handleOpen))
398452

453+
if (socket.readyState === WebSocket.OPEN) handleOpen()
454+
399455
const decoder = new TextDecoder()
400456

401457
const handleMessage = (event: MessageEvent) => {
@@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => {
462518

463519
onCleanup(() => {
464520
disposed = true
521+
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
522+
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
465523
output?.flush()
466524
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
467525
cleanup()
@@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => {
477535
classList={{
478536
...(local.classList ?? {}),
479537
"select-text": true,
480-
"size-full px-6 py-3 font-mono": true,
538+
"size-full px-6 py-3 font-mono relative overflow-hidden": true,
481539
[local.class ?? ""]: !!local.class,
482540
}}
483541
{...others}

0 commit comments

Comments
 (0)