mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-13 20:09:55 +00:00
* feat(web): improve mobile terminal input Co-Authored-By: Codex <noreply@anthropic.com> * fix(web): preserve terminal IME and key activation --------- Co-authored-by: Codex <noreply@anthropic.com>
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { Terminal } from '@xterm/xterm'
|
|
import { FitAddon } from '@xterm/addon-fit'
|
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
import { CanvasAddon } from '@xterm/addon-canvas'
|
|
import '@xterm/xterm/css/xterm.css'
|
|
import { ensureBuiltinFontLoaded, getFontProvider } from '@/lib/terminalFont'
|
|
import { getInitialTerminalFontSize } from '@/hooks/useTerminalFontSize'
|
|
|
|
function resolveThemeColors(): { background: string; foreground: string; selectionBackground: string } {
|
|
const styles = getComputedStyle(document.documentElement)
|
|
const background = styles.getPropertyValue('--app-bg').trim() || '#000000'
|
|
const foreground = styles.getPropertyValue('--app-fg').trim() || '#ffffff'
|
|
const selectionBackground = styles.getPropertyValue('--app-subtle-bg').trim() || 'rgba(255, 255, 255, 0.2)'
|
|
return { background, foreground, selectionBackground }
|
|
}
|
|
|
|
export function TerminalView(props: {
|
|
onMount?: (terminal: Terminal) => void
|
|
onResize?: (cols: number, rows: number) => void
|
|
className?: string
|
|
disableStdin?: boolean
|
|
}) {
|
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
const terminalRef = useRef<Terminal | null>(null)
|
|
const onMountRef = useRef(props.onMount)
|
|
const onResizeRef = useRef(props.onResize)
|
|
|
|
useEffect(() => {
|
|
onMountRef.current = props.onMount
|
|
}, [props.onMount])
|
|
|
|
useEffect(() => {
|
|
onResizeRef.current = props.onResize
|
|
}, [props.onResize])
|
|
|
|
useEffect(() => {
|
|
const terminal = terminalRef.current
|
|
if (!terminal) return
|
|
terminal.options.disableStdin = props.disableStdin ?? false
|
|
if (props.disableStdin) {
|
|
terminal.blur()
|
|
}
|
|
}, [props.disableStdin])
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current
|
|
if (!container) return
|
|
|
|
const abortController = new AbortController()
|
|
|
|
const fontProvider = getFontProvider()
|
|
const fontSize = getInitialTerminalFontSize()
|
|
const { background, foreground, selectionBackground } = resolveThemeColors()
|
|
const terminal = new Terminal({
|
|
cursorBlink: true,
|
|
disableStdin: props.disableStdin ?? false,
|
|
fontFamily: fontProvider.getFontFamily(),
|
|
fontSize,
|
|
theme: {
|
|
background,
|
|
foreground,
|
|
cursor: foreground,
|
|
selectionBackground
|
|
},
|
|
convertEol: true,
|
|
customGlyphs: true
|
|
})
|
|
|
|
const fitAddon = new FitAddon()
|
|
const webLinksAddon = new WebLinksAddon()
|
|
const canvasAddon = new CanvasAddon()
|
|
terminal.loadAddon(fitAddon)
|
|
terminal.loadAddon(webLinksAddon)
|
|
terminal.loadAddon(canvasAddon)
|
|
terminal.open(container)
|
|
terminalRef.current = terminal
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
requestAnimationFrame(() => {
|
|
fitAddon.fit()
|
|
onResizeRef.current?.(terminal.cols, terminal.rows)
|
|
})
|
|
})
|
|
observer.observe(container)
|
|
|
|
const refreshFont = (forceRemeasure = false) => {
|
|
if (abortController.signal.aborted) return
|
|
const nextFamily = fontProvider.getFontFamily()
|
|
|
|
if (forceRemeasure && terminal.options.fontFamily === nextFamily) {
|
|
terminal.options.fontFamily = `${nextFamily}, "__hapi_font_refresh__"`
|
|
requestAnimationFrame(() => {
|
|
if (abortController.signal.aborted) return
|
|
terminal.options.fontFamily = nextFamily
|
|
if (terminal.rows > 0) {
|
|
terminal.refresh(0, terminal.rows - 1)
|
|
}
|
|
fitAddon.fit()
|
|
onResizeRef.current?.(terminal.cols, terminal.rows)
|
|
})
|
|
return
|
|
}
|
|
|
|
terminal.options.fontFamily = nextFamily
|
|
if (terminal.rows > 0) {
|
|
terminal.refresh(0, terminal.rows - 1)
|
|
}
|
|
fitAddon.fit()
|
|
onResizeRef.current?.(terminal.cols, terminal.rows)
|
|
}
|
|
|
|
void ensureBuiltinFontLoaded().then(loaded => {
|
|
if (!loaded) return
|
|
refreshFont(true)
|
|
})
|
|
|
|
// Cleanup on abort
|
|
abortController.signal.addEventListener('abort', () => {
|
|
observer.disconnect()
|
|
fitAddon.dispose()
|
|
webLinksAddon.dispose()
|
|
canvasAddon.dispose()
|
|
terminal.dispose()
|
|
terminalRef.current = null
|
|
})
|
|
|
|
requestAnimationFrame(() => {
|
|
fitAddon.fit()
|
|
onResizeRef.current?.(terminal.cols, terminal.rows)
|
|
})
|
|
onMountRef.current?.(terminal)
|
|
|
|
return () => abortController.abort()
|
|
}, [])
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`h-full w-full ${props.className ?? ''}`}
|
|
/>
|
|
)
|
|
}
|