Last active
February 22, 2024 21:14
-
-
Save unframework/6983a5de51708fd72cea869374126929 to your computer and use it in GitHub Desktop.
Snapshot of a Xterm.js + Ink component in React and cross-compilation settings to bundle Ink for the browser environment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shim for Ink | |
module.exports = { | |
show: () => undefined, // no-op | |
hide: () => undefined, // no-op | |
toggle: () => undefined // no-op | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shim for Ink | |
module.exports = () => { | |
// no-op, also return stub unsubscribe function | |
return () => undefined; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shim for Ink | |
module.exports = { | |
stdout: { | |
level: 3, | |
hasBasic: true, | |
has256: true, | |
has16m: true | |
}, | |
stderr: { | |
level: 3, | |
hasBasic: true, | |
has256: true, | |
has16m: true | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// config Webpack with module shims to be able to bundle Ink for in-browser environment | |
// (in addition to normal TypeScript compilation) | |
// also install the following modules for correct shimming: buffer, stream-browserify, events | |
const path = require('path'); | |
const webpack = require('webpack'); | |
const HtmlWebpackPlugin = require('html-webpack-plugin'); | |
module.exports = { | |
entry: { | |
index: './src/index.ts' | |
}, | |
output: { | |
path: path.resolve(__dirname, 'dist'), | |
publicPath: '/', | |
filename: '[name]_bundle.[hash].js' | |
}, | |
resolve: { | |
extensions: ['.tsx', '.ts', '.js', '.jsx'], | |
fallback: { | |
assert: false, | |
buffer: require.resolve('buffer'), | |
stream: require.resolve('stream-browserify'), | |
fs: false, | |
module: false, | |
child_process: false | |
}, | |
alias: { | |
'supports-color$': path.resolve( | |
__dirname, | |
'webpack-stubs/supports-color.js' | |
), | |
'signal-exit$': path.resolve(__dirname, 'webpack-stubs/signal-exit.js'), | |
'window-size$': path.resolve(__dirname, 'webpack-stubs/window-size.js'), | |
'cli-cursor$': path.resolve(__dirname, 'webpack-stubs/cli-cursor.js') // cli-cursor does not get passed custom stderr, etc | |
} | |
}, | |
module: { | |
rules: [ | |
{ test: /\.(jsx?|tsx?)$/, use: 'ts-loader', exclude: /node_modules/ }, | |
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }, | |
{ | |
test: /\.(jpe?g|png|gif|svg|flac|wav)$/i, | |
use: 'file-loader?name=assets/[name].[hash].[ext]' | |
} | |
] | |
}, | |
plugins: [ | |
new webpack.DefinePlugin({ | |
process: '({ cwd: () => "/" })', // needed inside Ink's ErrorOverview | |
'process.env': '({})' // ci-info module references the entire env object | |
}), | |
new HtmlWebpackPlugin({ | |
chunks: ['index'], | |
filename: 'index.html', | |
template: 'src/index.html' | |
}) | |
], | |
devServer: { | |
inline: true | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shim for Ink | |
module.exports = { | |
width: 50, | |
height: 20, | |
get: () => { | |
return { | |
width: 50, | |
height: 20 | |
}; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { EventEmitter } from 'events'; | |
import { Terminal } from 'xterm'; | |
import { render } from 'ink'; | |
/// <reference types="node" /> | |
import 'xterm/css/xterm.css'; | |
// this spins up Xterm.js, initializes Ink inside it and sets up | |
// the component's children to be rendered inside Ink renderer | |
export const XtermCanvas: React.FC<{ | |
columns: number; // console width in chars | |
rows: number; // console height in chars | |
canvasContextRef: React.MutableRefObject< | |
CanvasRenderingContext2D | undefined | |
>; // this is filled in with canvas context, ready to be used in a Three.js texture, etc | |
}> = ({ columns, rows, canvasContextRef, children }) => { | |
// read once (no support for resizing) | |
const columnsRef = useRef(columns); | |
const rowsRef = useRef(rows); | |
const containerRef = useRef<HTMLDivElement>(null); | |
useEffect(() => { | |
if (!containerRef.current) { | |
throw new Error('no container'); | |
} | |
const xterm = new Terminal({ | |
allowTransparency: true, // turn off subpixel rendering per https://github.com/xtermjs/xterm.js/issues/1550#issuecomment-412246263 | |
scrollback: 0, | |
convertEol: true, // the Ink output contains only LF, not CRLF | |
fontSize: 10, | |
fontFamily: 'Inconsolata', | |
lineHeight: 1, | |
cols: columnsRef.current, | |
rows: rowsRef.current | |
}); | |
xterm.open(containerRef.current); | |
const sourceCanvas = containerRef.current.querySelector( | |
'.xterm-text-layer' | |
) as HTMLCanvasElement; | |
canvasContextRef.current = | |
(sourceCanvas && sourceCanvas.getContext('2d')) || undefined; | |
// wire up to Ink | |
// @todo typings | |
const inputStream = new EventEmitter() as any; | |
xterm.onData(data => { | |
inputStream.emit('data', data); | |
}); | |
const outputStream = new EventEmitter() as any; // NodeJS.WriteStream; | |
outputStream.columns = columnsRef.current; | |
outputStream.rows = rowsRef.current; | |
outputStream.writable = true; | |
outputStream.write = ( | |
data: unknown, | |
encoding: unknown, | |
callback: unknown | |
) => { | |
// handle typical write scenarios and fail on unsupported ones | |
if (typeof encoding === 'function') { | |
callback = encoding; | |
encoding = undefined; | |
} | |
if (typeof data !== 'string') { | |
throw new Error('writing non-strings not supported'); | |
} | |
if (encoding && encoding !== 'utf-8') { | |
throw new Error('writing non-UTF-8 encodings not supported'); | |
} | |
if (callback) { | |
throw new Error('write callback not supported'); | |
} | |
// pass on data to the terminal display | |
xterm.write(data); | |
}; | |
render(<>{children}</>, { | |
stdin: inputStream, | |
stdout: outputStream, | |
stderr: outputStream, | |
patchConsole: false | |
}); | |
}, []); | |
// position terminal canvas within screen bounds, but clipped by zero-size container | |
// (if it is out of bounds, updates do not happen) | |
return ReactDOM.createPortal( | |
<div | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: '0px', | |
height: '0px', | |
overflow: 'hidden' | |
}} | |
ref={containerRef} | |
/>, | |
document.body | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment