feat(web): add built-in Nerd Font for terminal icon support (#122)

* feat(web): add built-in Nerd Font for terminal icon support

Co-authored-by: tfq <tfq@gmail.com>
Co-authored-by: weishu <twsxtd@gmail.com>
This commit is contained in:
遗憾组成人生
2026-02-02 11:17:48 +08:00
committed by GitHub
co-authored by tfq weishu
parent 5b27f6b41e
commit 61dd69d5df
2 changed files with 175 additions and 74 deletions
+51 -21
View File
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react'
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 { createFontProvider, type ITerminalFontProvider } from '@/lib/terminalFont'
import { ensureBuiltinFontLoaded, getFontProvider } from '@/lib/terminalFont'
function resolveThemeColors(): { background: string; foreground: string; selectionBackground: string } {
const styles = getComputedStyle(document.documentElement)
@@ -22,12 +22,6 @@ export function TerminalView(props: {
const containerRef = useRef<HTMLDivElement | null>(null)
const onMountRef = useRef(props.onMount)
const onResizeRef = useRef(props.onResize)
const [fontProvider, setFontProvider] = useState<ITerminalFontProvider | null>(null)
// Initialize font provider
useEffect(() => {
createFontProvider('default').then(setFontProvider)
}, [])
useEffect(() => {
onMountRef.current = props.onMount
@@ -39,10 +33,11 @@ export function TerminalView(props: {
useEffect(() => {
const container = containerRef.current
if (!container || !fontProvider) {
return
}
if (!container) return
const abortController = new AbortController()
const fontProvider = getFontProvider()
const { background, foreground, selectionBackground } = resolveThemeColors()
const terminal = new Terminal({
cursorBlink: true,
@@ -66,27 +61,62 @@ export function TerminalView(props: {
terminal.loadAddon(canvasAddon)
terminal.open(container)
const resizeTerminal = () => {
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)
}
const observer = new ResizeObserver(() => {
requestAnimationFrame(resizeTerminal)
void ensureBuiltinFontLoaded().then(loaded => {
if (!loaded) return
refreshFont(true)
})
observer.observe(container)
requestAnimationFrame(resizeTerminal)
onMountRef.current?.(terminal)
return () => {
// Cleanup on abort
abortController.signal.addEventListener('abort', () => {
observer.disconnect()
fitAddon.dispose()
webLinksAddon.dispose()
canvasAddon.dispose()
terminal.dispose()
}
}, [fontProvider])
})
requestAnimationFrame(() => {
fitAddon.fit()
onResizeRef.current?.(terminal.cols, terminal.rows)
})
onMountRef.current?.(terminal)
return () => abortController.abort()
}, [])
return (
<div