Skip to content

Instantly share code, notes, and snippets.

@mastersign
Created December 1, 2023 11:34
Show Gist options
  • Save mastersign/90d0ab06f040092e4ca27a3b59820cb9 to your computer and use it in GitHub Desktop.
Save mastersign/90d0ab06f040092e4ca27a3b59820cb9 to your computer and use it in GitHub Desktop.
XTermJS for React 18
import { useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { Terminal } from 'xterm'
import { Unicode11Addon } from 'xterm-addon-unicode11'
import { CanvasAddon } from 'xterm-addon-canvas'
import { WebglAddon } from 'xterm-addon-webgl';
import { WebLinksAddon } from 'xterm-addon-web-links'
import FontFaceObserver from 'fontfaceobserver'
import 'xterm/css/xterm.css'
const isWebGl2Supported = !!document.createElement('canvas').getContext('webgl2')
async function waitForWebFontAsync(term) {
const fontFamily = term.options.fontFamily
if (!fontFamily) return
const regular = new FontFaceObserver(fontFamily).load()
//const bold = new FontFaceObserver(fontFamily, { weight: 'bold' }).load()
try {
await regular
//await bold
} catch {
// loading the font failed, fallback to browser default
term.options.fontFamily = "monospace"
}
}
function useBind(termRef, handler, eventName) {
useEffect(() => {
if (!termRef.current || typeof handler !== 'function') return
// console.log('Bind', eventName)
const term = termRef.current
const eventBinding = term[eventName](handler)
return () => {
if (!eventBinding) return
// console.log('Unbind', eventName)
eventBinding.dispose()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handler])
}
export function XTerm({
id, className,
options,
onBell, onBinary, onCursorMove, onData, onKey, onLineFeed, onRender, onResize, onScroll, onSelectionChange, onTitleChange, onWriteParsed,
onInit, onDispose,
}) {
const termRef = useRef(null)
const divRef = useRef(null)
useEffect(() => {
console.log('Mount XTerm')
const term = new Terminal({ ...options, allowProposedApi: true })
term.loadAddon(new Unicode11Addon())
term.unicode.activeVersion = '11'
term.loadAddon(new WebLinksAddon())
if (isWebGl2Supported) {
const webGl2Addon = new WebglAddon();
webGl2Addon.onContextLoss(() => {
webGl2Addon.dispose();
});
term.loadAddon(webGl2Addon);
} else {
term.loadAddon(new CanvasAddon())
}
termRef.current = term
waitForWebFontAsync(term).then(() => {
if (termRef.current !== term) {
// component was disposed while loading web fonts
return
}
term.open(divRef.current)
})
return () => {
console.log('Unmount XTerm')
if (typeof onDispose === 'function') onDispose(term)
try {
term.dispose()
} catch (ex) {
console.warn('An error occurred during disposal of the XTermJS')
}
termRef.current = null
}
}, [options, onDispose])
useBind(termRef, onBell, 'onBell')
useBind(termRef, onBinary, 'onBinary')
useBind(termRef, onCursorMove, 'onCursorMove')
useBind(termRef, onData, 'onData')
useBind(termRef, onKey, 'onKey')
useBind(termRef, onLineFeed, 'onLineFeed')
useBind(termRef, onRender, 'onRender')
useBind(termRef, onResize, 'onResize')
useBind(termRef, onScroll, 'onScroll')
useBind(termRef, onSelectionChange, 'onSelectionChange')
useBind(termRef, onTitleChange, 'onTitleChange')
useBind(termRef, onWriteParsed, 'onWriteParsed')
useEffect(() => {
if (!termRef.current || typeof onInit !== 'function') return
console.log('onInit')
onInit(termRef.current)
}, [options, onInit])
return <div id={id} className={className} ref={divRef}></div>
}
XTerm.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
options: PropTypes.object,
onBell: PropTypes.func,
onBinary: PropTypes.func,
onCursorMove: PropTypes.func,
onData: PropTypes.func,
onKey: PropTypes.func,
onLineFeed: PropTypes.func,
onRender: PropTypes.func,
onResize: PropTypes.func,
onScroll: PropTypes.func,
onSelectionChange: PropTypes.func,
onTitleChange: PropTypes.func,
onWriteParsed: PropTypes.func,
onInit: PropTypes.func,
onDispose: PropTypes.func,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment