Skip to content

Instantly share code, notes, and snippets.

@wezzels
Forked from unframework/XtermCanvas.tsx
Created April 19, 2022 02:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wezzels/2c509f0c95becb10b727f54b98bba169 to your computer and use it in GitHub Desktop.
Save wezzels/2c509f0c95becb10b727f54b98bba169 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
// shim for Ink
module.exports = {
show: () => undefined, // no-op
hide: () => undefined, // no-op
toggle: () => undefined // no-op
};
// shim for Ink
module.exports = () => {
// no-op, also return stub unsubscribe function
return () => undefined;
};
// shim for Ink
module.exports = {
stdout: {
level: 3,
hasBasic: true,
has256: true,
has16m: true
},
stderr: {
level: 3,
hasBasic: true,
has256: true,
has16m: true
}
};
// 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
}
};
// shim for Ink
module.exports = {
width: 50,
height: 20,
get: () => {
return {
width: 50,
height: 20
};
}
};
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