|
|
@@ -156,6 +156,10 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
let serializeAddon: SerializeAddon
|
|
|
let fitAddon: FitAddon
|
|
|
let handleResize: () => void
|
|
|
+ let fitFrame: number | undefined
|
|
|
+ let sizeTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
+ let pendingSize: { cols: number; rows: number } | undefined
|
|
|
+ let lastSize: { cols: number; rows: number } | undefined
|
|
|
let disposed = false
|
|
|
const cleanups: VoidFunction[] = []
|
|
|
const start =
|
|
|
@@ -209,6 +213,43 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
|
|
|
|
|
|
+ const scheduleFit = () => {
|
|
|
+ if (disposed) return
|
|
|
+ if (!fitAddon) return
|
|
|
+ if (fitFrame !== undefined) return
|
|
|
+
|
|
|
+ fitFrame = requestAnimationFrame(() => {
|
|
|
+ fitFrame = undefined
|
|
|
+ if (disposed) return
|
|
|
+ fitAddon.fit()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const scheduleSize = (cols: number, rows: number) => {
|
|
|
+ if (disposed) return
|
|
|
+ if (lastSize?.cols === cols && lastSize?.rows === rows) return
|
|
|
+
|
|
|
+ pendingSize = { cols, rows }
|
|
|
+
|
|
|
+ if (!lastSize) {
|
|
|
+ lastSize = pendingSize
|
|
|
+ void pushSize(cols, rows)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sizeTimer !== undefined) return
|
|
|
+ sizeTimer = setTimeout(() => {
|
|
|
+ sizeTimer = undefined
|
|
|
+ const next = pendingSize
|
|
|
+ if (!next) return
|
|
|
+ pendingSize = undefined
|
|
|
+ if (disposed) return
|
|
|
+ if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return
|
|
|
+ lastSize = next
|
|
|
+ void pushSize(next.cols, next.rows)
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+
|
|
|
createEffect(() => {
|
|
|
const colors = getTerminalColors()
|
|
|
setTerminalColors(colors)
|
|
|
@@ -220,6 +261,16 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
const font = monoFontFamily(settings.appearance.font())
|
|
|
if (!term) return
|
|
|
setOptionIfSupported(term, "fontFamily", font)
|
|
|
+ scheduleFit()
|
|
|
+ })
|
|
|
+
|
|
|
+ let zoom = platform.webviewZoom?.()
|
|
|
+ createEffect(() => {
|
|
|
+ const next = platform.webviewZoom?.()
|
|
|
+ if (next === undefined) return
|
|
|
+ if (next === zoom) return
|
|
|
+ zoom = next
|
|
|
+ scheduleFit()
|
|
|
})
|
|
|
|
|
|
const focusTerminal = () => {
|
|
|
@@ -263,25 +314,6 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
const once = { value: false }
|
|
|
|
|
|
- const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
|
|
- url.searchParams.set("directory", sdk.directory)
|
|
|
- url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
|
|
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
|
|
- if (window.__OPENCODE__?.serverPassword) {
|
|
|
- url.username = "opencode"
|
|
|
- url.password = window.__OPENCODE__?.serverPassword
|
|
|
- }
|
|
|
- const socket = new WebSocket(url)
|
|
|
- socket.binaryType = "arraybuffer"
|
|
|
- cleanups.push(() => {
|
|
|
- if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
|
|
- })
|
|
|
- if (disposed) {
|
|
|
- cleanup()
|
|
|
- return
|
|
|
- }
|
|
|
- ws = socket
|
|
|
-
|
|
|
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
|
|
const restoreSize =
|
|
|
restore &&
|
|
|
@@ -344,9 +376,28 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
focusTerminal()
|
|
|
|
|
|
+ if (typeof document !== "undefined" && document.fonts) {
|
|
|
+ document.fonts.ready.then(scheduleFit)
|
|
|
+ }
|
|
|
+
|
|
|
+ const onResize = t.onResize((size) => {
|
|
|
+ scheduleSize(size.cols, size.rows)
|
|
|
+ })
|
|
|
+ cleanups.push(() => disposeIfDisposable(onResize))
|
|
|
+ const onData = t.onData((data) => {
|
|
|
+ if (ws?.readyState === WebSocket.OPEN) ws.send(data)
|
|
|
+ })
|
|
|
+ cleanups.push(() => disposeIfDisposable(onData))
|
|
|
+ const onKey = t.onKey((key) => {
|
|
|
+ if (key.key == "Enter") {
|
|
|
+ props.onSubmit?.()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ cleanups.push(() => disposeIfDisposable(onKey))
|
|
|
+
|
|
|
const startResize = () => {
|
|
|
fit.observeResize()
|
|
|
- handleResize = () => fit.fit()
|
|
|
+ handleResize = scheduleFit
|
|
|
window.addEventListener("resize", handleResize)
|
|
|
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
|
|
}
|
|
|
@@ -354,11 +405,13 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
if (restore && restoreSize) {
|
|
|
t.write(restore, () => {
|
|
|
fit.fit()
|
|
|
+ scheduleSize(t.cols, t.rows)
|
|
|
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
|
|
startResize()
|
|
|
})
|
|
|
} else {
|
|
|
fit.fit()
|
|
|
+ scheduleSize(t.cols, t.rows)
|
|
|
if (restore) {
|
|
|
t.write(restore, () => {
|
|
|
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
|
|
@@ -367,35 +420,38 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
startResize()
|
|
|
}
|
|
|
|
|
|
- const onResize = t.onResize(async (size) => {
|
|
|
- if (socket.readyState === WebSocket.OPEN) {
|
|
|
- await pushSize(size.cols, size.rows)
|
|
|
- }
|
|
|
- })
|
|
|
- cleanups.push(() => disposeIfDisposable(onResize))
|
|
|
- const onData = t.onData((data) => {
|
|
|
- if (socket.readyState === WebSocket.OPEN) {
|
|
|
- socket.send(data)
|
|
|
- }
|
|
|
- })
|
|
|
- cleanups.push(() => disposeIfDisposable(onData))
|
|
|
- const onKey = t.onKey((key) => {
|
|
|
- if (key.key == "Enter") {
|
|
|
- props.onSubmit?.()
|
|
|
- }
|
|
|
- })
|
|
|
- cleanups.push(() => disposeIfDisposable(onKey))
|
|
|
// t.onScroll((ydisp) => {
|
|
|
// console.log("Scroll position:", ydisp)
|
|
|
// })
|
|
|
|
|
|
+ const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
|
|
+ url.searchParams.set("directory", sdk.directory)
|
|
|
+ url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
|
|
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
|
|
+ if (window.__OPENCODE__?.serverPassword) {
|
|
|
+ url.username = "opencode"
|
|
|
+ url.password = window.__OPENCODE__?.serverPassword
|
|
|
+ }
|
|
|
+ const socket = new WebSocket(url)
|
|
|
+ socket.binaryType = "arraybuffer"
|
|
|
+ ws = socket
|
|
|
+ cleanups.push(() => {
|
|
|
+ if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
|
|
+ })
|
|
|
+ if (disposed) {
|
|
|
+ cleanup()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
const handleOpen = () => {
|
|
|
local.onConnect?.()
|
|
|
- void pushSize(t.cols, t.rows)
|
|
|
+ scheduleSize(t.cols, t.rows)
|
|
|
}
|
|
|
socket.addEventListener("open", handleOpen)
|
|
|
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
|
|
|
|
|
+ if (socket.readyState === WebSocket.OPEN) handleOpen()
|
|
|
+
|
|
|
const decoder = new TextDecoder()
|
|
|
|
|
|
const handleMessage = (event: MessageEvent) => {
|
|
|
@@ -462,6 +518,8 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
onCleanup(() => {
|
|
|
disposed = true
|
|
|
+ if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
|
|
+ if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
|
|
output?.flush()
|
|
|
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
|
|
cleanup()
|
|
|
@@ -477,7 +535,7 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
classList={{
|
|
|
...(local.classList ?? {}),
|
|
|
"select-text": true,
|
|
|
- "size-full px-6 py-3 font-mono": true,
|
|
|
+ "size-full px-6 py-3 font-mono relative overflow-hidden": true,
|
|
|
[local.class ?? ""]: !!local.class,
|
|
|
}}
|
|
|
{...others}
|