Przeglądaj źródła

fix(app): terminal resize

Adam 2 miesięcy temu
rodzic
commit
1c71604e0a
1 zmienionych plików z 98 dodań i 40 usunięć
  1. 98 40
      packages/app/src/components/terminal.tsx

+ 98 - 40
packages/app/src/components/terminal.tsx

@@ -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}