2016-07-14 15:40:15 -08:00
|
|
|
/* global Blob,URL,requestAnimationFrame */
|
2016-07-13 12:44:24 -08:00
|
|
|
import React from 'react';
|
2017-09-03 11:15:33 -08:00
|
|
|
import Terminal from 'xterm';
|
2017-09-16 11:17:22 -08:00
|
|
|
import {PureComponent} from '../base-components';
|
2017-02-23 02:10:05 -09:00
|
|
|
import terms from '../terms';
|
2017-09-03 11:15:33 -08:00
|
|
|
import returnKey from '../utils/keymaps';
|
|
|
|
|
import CommandRegistry from '../command-registry';
|
2017-06-11 02:42:39 -08:00
|
|
|
|
|
|
|
|
// map old hterm constants to xterm.js
|
|
|
|
|
const CURSOR_STYLES = {
|
|
|
|
|
BEAM: 'bar',
|
|
|
|
|
UNDERLINE: 'underline',
|
|
|
|
|
BLOCK: 'block'
|
2017-09-10 05:35:10 -08:00
|
|
|
};
|
2016-06-30 22:01:04 -08:00
|
|
|
|
2017-09-16 11:17:22 -08:00
|
|
|
export default class Term extends PureComponent {
|
2016-09-21 06:27:11 -08:00
|
|
|
constructor(props) {
|
2016-07-13 12:44:24 -08:00
|
|
|
super(props);
|
2017-09-09 03:42:19 -08:00
|
|
|
props.ref_(props.uid, this);
|
2017-09-10 05:35:10 -08:00
|
|
|
this.termRef = null;
|
|
|
|
|
this.termWrapperRef = null;
|
|
|
|
|
this.termRect = null;
|
|
|
|
|
this.onOpen = this.onOpen.bind(this);
|
|
|
|
|
this.onWindowResize = this.onWindowResize.bind(this);
|
|
|
|
|
this.onTermRef = this.onTermRef.bind(this);
|
|
|
|
|
this.onTermWrapperRef = this.onTermWrapperRef.bind(this);
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
componentDidMount() {
|
|
|
|
|
const {props} = this;
|
2016-06-30 22:01:04 -08:00
|
|
|
|
2017-08-02 11:05:47 -08:00
|
|
|
// we need to use this hack to retain the term reference
|
|
|
|
|
// as we move the term around splits, until xterm adds
|
|
|
|
|
// support for getState / setState
|
|
|
|
|
if (props.term) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term = props.term;
|
|
|
|
|
this.termRef.appendChild(this.term.element);
|
|
|
|
|
this.onOpen();
|
2017-08-02 11:05:47 -08:00
|
|
|
} else {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term =
|
|
|
|
|
props.term ||
|
|
|
|
|
new Terminal({
|
|
|
|
|
cursorStyle: CURSOR_STYLES[props.cursorShape],
|
|
|
|
|
cursorBlink: props.cursorBlink
|
|
|
|
|
});
|
|
|
|
|
this.term.attachCustomKeyEventHandler(this.keyboardHandler);
|
|
|
|
|
this.term.on('open', this.onOpen);
|
2017-08-02 11:05:47 -08:00
|
|
|
this.term.open(this.termRef, {
|
|
|
|
|
focus: false
|
2017-09-10 05:35:10 -08:00
|
|
|
});
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
2016-10-19 14:55:06 -08:00
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onTitle) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term.on('title', props.onTitle);
|
2017-06-11 02:42:39 -08:00
|
|
|
}
|
2016-07-03 12:35:45 -08:00
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onActive) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term.on('focus', props.onActive);
|
2016-08-05 14:30:40 -08:00
|
|
|
}
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onData) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term.on('data', props.onData);
|
2016-08-13 13:03:44 -08:00
|
|
|
}
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onResize) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term.on('resize', ({cols, rows}) => {
|
|
|
|
|
props.onResize(cols, rows);
|
|
|
|
|
});
|
2016-09-21 06:27:11 -08:00
|
|
|
}
|
2016-07-13 12:44:24 -08:00
|
|
|
|
2017-08-02 11:05:47 -08:00
|
|
|
window.addEventListener('resize', this.onWindowResize, {
|
|
|
|
|
passive: true
|
2017-09-10 05:35:10 -08:00
|
|
|
});
|
2016-10-03 18:00:50 -08:00
|
|
|
|
2017-02-23 02:10:05 -09:00
|
|
|
terms[this.props.uid] = this;
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
onOpen() {
|
2017-08-02 11:05:47 -08:00
|
|
|
// we need to delay one frame so that aphrodite styles
|
|
|
|
|
// get applied and we can make an accurate measurement
|
|
|
|
|
// of the container width and height
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
// at this point it would make sense for character
|
|
|
|
|
// measurement to have taken place but it seems that
|
|
|
|
|
// xterm.js might be doing this asynchronously, so
|
|
|
|
|
// we force it instead
|
|
|
|
|
this.term.charMeasure.measure();
|
|
|
|
|
this.measureResize();
|
2017-09-10 05:35:10 -08:00
|
|
|
});
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
getTermDocument() {
|
2017-06-11 02:42:39 -08:00
|
|
|
// eslint-disable-next-line no-console
|
2017-09-10 05:35:10 -08:00
|
|
|
console.error('unimplemented');
|
2016-12-21 06:19:41 -09:00
|
|
|
}
|
2016-11-16 09:44:04 -09:00
|
|
|
|
2017-08-02 11:05:47 -08:00
|
|
|
// measures the container and makes the decision
|
|
|
|
|
// whether to resize the term to fit the container
|
2017-09-10 05:35:10 -08:00
|
|
|
measureResize() {
|
|
|
|
|
//eslint-disable-next-line no-console
|
|
|
|
|
console.log('performing measure resize');
|
|
|
|
|
const termRect = this.termWrapperRef.getBoundingClientRect();
|
2017-08-02 11:05:47 -08:00
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
if (!this.termRect || termRect.width !== this.termRect.width || termRect.height !== this.termRect.height) {
|
2017-08-02 11:05:47 -08:00
|
|
|
this.termRect = termRect;
|
2017-09-10 05:35:10 -08:00
|
|
|
//eslint-disable-next-line no-console
|
|
|
|
|
console.log('performing fit resize');
|
|
|
|
|
this.fitResize();
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
onWindowResize() {
|
2017-08-02 11:05:47 -08:00
|
|
|
this.measureResize();
|
2016-10-03 18:00:50 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
write(data) {
|
2017-06-11 02:42:39 -08:00
|
|
|
this.term.write(data);
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
focus() {
|
2017-06-11 02:42:39 -08:00
|
|
|
this.term.focus();
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
clear() {
|
2017-06-11 02:42:39 -08:00
|
|
|
this.term.clear();
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
reset() {
|
2017-08-02 11:05:47 -08:00
|
|
|
this.term.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
resize(cols, rows) {
|
2017-08-02 11:05:47 -08:00
|
|
|
this.term.resize(cols, rows);
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
fitResize() {
|
|
|
|
|
const cols = Math.floor(this.termRect.width / this.term.charMeasure.width);
|
|
|
|
|
const rows = Math.floor(this.termRect.height / this.term.charMeasure.height);
|
2017-08-02 11:05:47 -08:00
|
|
|
|
|
|
|
|
if (cols !== this.props.cols || rows !== this.props.rows) {
|
2017-09-10 05:35:10 -08:00
|
|
|
this.resize(cols, rows);
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
2016-07-03 12:35:45 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-03 11:15:33 -08:00
|
|
|
keyboardHandler(e) {
|
|
|
|
|
// test key from keymaps before moving forward with actions
|
|
|
|
|
const key = returnKey(e);
|
|
|
|
|
if (key) {
|
|
|
|
|
if (CommandRegistry.getCommand(key)) {
|
|
|
|
|
CommandRegistry.exec(key, e);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
componentWillReceiveProps(nextProps) {
|
2016-07-13 12:44:24 -08:00
|
|
|
if (!this.props.cleared && nextProps.cleared) {
|
|
|
|
|
this.clear();
|
2016-07-05 12:14:30 -08:00
|
|
|
}
|
2017-08-02 11:05:47 -08:00
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
if (this.props.fontSize !== nextProps.fontSize || this.props.fontFamily !== nextProps.fontFamily) {
|
2017-08-02 11:05:47 -08:00
|
|
|
// invalidate xterm cache about how wide each
|
|
|
|
|
// character is
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term.charMeasure.measure();
|
2017-08-02 11:05:47 -08:00
|
|
|
|
|
|
|
|
// resize to fit the container
|
2017-09-10 05:35:10 -08:00
|
|
|
this.fitResize();
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
if (nextProps.rows !== this.props.rows || nextProps.cols !== this.props.cols) {
|
|
|
|
|
this.resize(nextProps.cols, nextProps.rows);
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-09 03:42:19 -08:00
|
|
|
onTermWrapperRef(component) {
|
|
|
|
|
this.termWrapperRef = component;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onTermRef(component) {
|
|
|
|
|
this.termRef = component;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
componentWillUnmount() {
|
2017-09-09 03:42:19 -08:00
|
|
|
terms[this.props.uid] = null;
|
|
|
|
|
this.props.ref_(this.props.uid, null);
|
2017-08-02 11:05:47 -08:00
|
|
|
|
|
|
|
|
// to clean up the terminal, we remove the listeners
|
2017-09-10 05:35:10 -08:00
|
|
|
// instead of invoking `destroy`, since it will make the
|
2017-08-02 11:05:47 -08:00
|
|
|
// term insta un-attachable in the future (which we need
|
|
|
|
|
// to do in case of splitting, see `componentDidMount`
|
2017-09-10 05:35:10 -08:00
|
|
|
this.term._events = {};
|
2017-08-02 11:05:47 -08:00
|
|
|
|
|
|
|
|
window.removeEventListener('resize', this.onWindowResize, {
|
|
|
|
|
passive: true
|
2017-09-10 05:35:10 -08:00
|
|
|
});
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
template(css) {
|
2017-09-10 05:35:10 -08:00
|
|
|
return (
|
|
|
|
|
<div className={css('fit', this.props.isTermActive && 'active')} style={{padding: this.props.padding}}>
|
|
|
|
|
{this.props.customChildrenBefore}
|
|
|
|
|
<div ref={this.onTermWrapperRef} className={css('fit', 'wrapper')}>
|
|
|
|
|
<div ref={this.onTermRef} className={css('term')} />
|
|
|
|
|
</div>
|
|
|
|
|
{this.props.customChildren}
|
2017-08-02 11:05:47 -08:00
|
|
|
</div>
|
2017-09-10 05:35:10 -08:00
|
|
|
);
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
styles() {
|
2016-07-13 12:44:24 -08:00
|
|
|
return {
|
|
|
|
|
fit: {
|
2016-10-03 18:00:50 -08:00
|
|
|
display: 'block',
|
2016-07-13 12:44:24 -08:00
|
|
|
width: '100%',
|
|
|
|
|
height: '100%'
|
|
|
|
|
},
|
2017-08-02 11:05:47 -08:00
|
|
|
wrapper: {
|
|
|
|
|
// TODO: decide whether to keep this or not based on
|
|
|
|
|
// understanding what xterm-selection is for
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
},
|
2017-06-11 02:42:39 -08:00
|
|
|
term: {}
|
2016-07-13 12:44:24 -08:00
|
|
|
};
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
}
|