2016-07-14 15:40:15 -08:00
|
|
|
/* global Blob,URL,requestAnimationFrame */
|
2016-07-13 12:44:24 -08:00
|
|
|
import React from 'react';
|
2018-01-09 07:33:24 -09:00
|
|
|
import {Terminal} from 'xterm';
|
2018-01-15 08:36:02 -09:00
|
|
|
import * as fit from 'xterm/lib/addons/fit/fit';
|
2018-04-05 12:45:18 -08:00
|
|
|
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
|
2018-04-05 13:53:21 -08:00
|
|
|
import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat';
|
2017-10-24 14:06:46 -08:00
|
|
|
import {clipboard} from 'electron';
|
2018-01-09 07:33:24 -09:00
|
|
|
import * as Color from 'color';
|
2017-02-23 02:10:05 -09:00
|
|
|
import terms from '../terms';
|
2017-10-22 13:16:52 -08:00
|
|
|
import processClipboard from '../utils/paste';
|
2017-06-11 02:42:39 -08:00
|
|
|
|
2018-01-15 08:36:02 -09:00
|
|
|
Terminal.applyAddon(fit);
|
2018-04-05 12:45:18 -08:00
|
|
|
Terminal.applyAddon(webLinks);
|
2018-04-05 13:53:21 -08:00
|
|
|
Terminal.applyAddon(winptyCompat);
|
2018-01-15 08:36:02 -09:00
|
|
|
|
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
|
|
|
|
2018-01-09 07:33:24 -09:00
|
|
|
const getTermOptions = props => {
|
|
|
|
|
// Set a background color only if it is opaque
|
2018-02-18 03:28:26 -09:00
|
|
|
const needTransparency = Color(props.backgroundColor).alpha() < 1;
|
|
|
|
|
const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor;
|
2018-01-09 07:33:24 -09:00
|
|
|
return {
|
2018-01-21 00:19:27 -09:00
|
|
|
macOptionIsMeta: props.modifierKeys.altIsMeta,
|
2018-05-29 02:11:21 -08:00
|
|
|
scrollback: props.scrollback,
|
2018-01-09 07:33:24 -09:00
|
|
|
cursorStyle: CURSOR_STYLES[props.cursorShape],
|
|
|
|
|
cursorBlink: props.cursorBlink,
|
|
|
|
|
fontFamily: props.fontFamily,
|
|
|
|
|
fontSize: props.fontSize,
|
2018-02-14 04:09:02 -09:00
|
|
|
fontWeight: props.fontWeight,
|
|
|
|
|
fontWeightBold: props.fontWeightBold,
|
2018-04-20 14:22:34 -08:00
|
|
|
lineHeight: props.lineHeight,
|
2018-05-10 02:54:00 -08:00
|
|
|
letterSpacing: props.letterSpacing,
|
2018-02-18 03:28:26 -09:00
|
|
|
allowTransparency: needTransparency,
|
2018-03-17 16:19:26 -08:00
|
|
|
experimentalCharAtlas: 'dynamic',
|
2018-01-09 07:33:24 -09:00
|
|
|
theme: {
|
|
|
|
|
foreground: props.foregroundColor,
|
|
|
|
|
background: backgroundColor,
|
|
|
|
|
cursor: props.cursorColor,
|
|
|
|
|
cursorAccent: props.cursorAccentColor,
|
|
|
|
|
selection: props.selectionColor,
|
|
|
|
|
black: props.colors.black,
|
|
|
|
|
red: props.colors.red,
|
|
|
|
|
green: props.colors.green,
|
|
|
|
|
yellow: props.colors.yellow,
|
|
|
|
|
blue: props.colors.blue,
|
|
|
|
|
magenta: props.colors.magenta,
|
|
|
|
|
cyan: props.colors.cyan,
|
|
|
|
|
white: props.colors.white,
|
|
|
|
|
brightBlack: props.colors.lightBlack,
|
|
|
|
|
brightRed: props.colors.lightRed,
|
|
|
|
|
brightGreen: props.colors.lightGreen,
|
|
|
|
|
brightYellow: props.colors.lightYellow,
|
|
|
|
|
brightBlue: props.colors.lightBlue,
|
|
|
|
|
brightMagenta: props.colors.lightMagenta,
|
|
|
|
|
brightCyan: props.colors.lightCyan,
|
|
|
|
|
brightWhite: props.colors.lightWhite
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2018-03-17 04:51:36 -08:00
|
|
|
export default class Term extends React.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);
|
2017-10-22 13:16:52 -08:00
|
|
|
this.onWindowPaste = this.onWindowPaste.bind(this);
|
2017-09-10 05:35:10 -08:00
|
|
|
this.onTermRef = this.onTermRef.bind(this);
|
|
|
|
|
this.onTermWrapperRef = this.onTermWrapperRef.bind(this);
|
2017-10-24 14:06:46 -08:00
|
|
|
this.onMouseUp = this.onMouseUp.bind(this);
|
2018-01-09 07:33:24 -09:00
|
|
|
this.termOptions = {};
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners = [];
|
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
|
|
|
|
2018-01-09 07:33:24 -09:00
|
|
|
this.termOptions = getTermOptions(props);
|
|
|
|
|
this.term = props.term || new Terminal(this.termOptions);
|
|
|
|
|
this.term.attachCustomKeyEventHandler(this.keyboardHandler);
|
|
|
|
|
this.term.open(this.termRef);
|
2018-04-05 12:45:18 -08:00
|
|
|
this.term.webLinksInit();
|
2018-04-05 13:53:21 -08:00
|
|
|
this.term.winptyCompatInit();
|
2018-04-05 12:45:18 -08:00
|
|
|
|
2017-08-02 11:05:47 -08:00
|
|
|
if (props.term) {
|
2018-01-09 07:33:24 -09:00
|
|
|
//We need to set options again after reattaching an existing term
|
|
|
|
|
Object.keys(this.termOptions).forEach(option => this.term.setOption(option, this.termOptions[option]));
|
2017-08-02 11:05:47 -08:00
|
|
|
}
|
2018-02-03 07:00:30 -09:00
|
|
|
if (this.props.isTermActive) {
|
|
|
|
|
this.term.focus();
|
|
|
|
|
}
|
2016-10-19 14:55:06 -08:00
|
|
|
|
2018-01-09 07:33:24 -09:00
|
|
|
this.onOpen(this.termOptions);
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onTitle) {
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.push(this.term.addDisposableListener('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) {
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.push(this.term.addDisposableListener('focus', props.onActive));
|
2016-08-05 14:30:40 -08:00
|
|
|
}
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onData) {
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.push(this.term.addDisposableListener('data', props.onData));
|
2016-08-13 13:03:44 -08:00
|
|
|
}
|
|
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
if (props.onResize) {
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.push(
|
|
|
|
|
this.term.addDisposableListener('resize', ({cols, rows}) => {
|
|
|
|
|
props.onResize(cols, rows);
|
|
|
|
|
})
|
|
|
|
|
);
|
2016-09-21 06:27:11 -08:00
|
|
|
}
|
2016-07-13 12:44:24 -08:00
|
|
|
|
2018-01-22 08:12:03 -09:00
|
|
|
if (props.onCursorMove) {
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.push(
|
|
|
|
|
this.term.addDisposableListener('cursormove', () => {
|
|
|
|
|
const cursorFrame = {
|
2018-12-15 12:30:58 -09:00
|
|
|
x: this.term._core.buffer.x * this.term._core.renderer.dimensions.actualCellWidth,
|
|
|
|
|
y: this.term._core.buffer.y * this.term._core.renderer.dimensions.actualCellHeight,
|
|
|
|
|
width: this.term._core.renderer.dimensions.actualCellWidth,
|
|
|
|
|
height: this.term._core.renderer.dimensions.actualCellHeight,
|
|
|
|
|
col: this.term._core.buffer.y,
|
|
|
|
|
row: this.term._core.buffer.x
|
2018-12-06 14:56:29 -09:00
|
|
|
};
|
|
|
|
|
props.onCursorMove(cursorFrame);
|
|
|
|
|
})
|
|
|
|
|
);
|
2018-01-22 08:12:03 -09: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-10-22 13:16:52 -08:00
|
|
|
window.addEventListener('paste', this.onWindowPaste, {
|
|
|
|
|
capture: true
|
|
|
|
|
});
|
|
|
|
|
|
2017-02-23 02:10:05 -09:00
|
|
|
terms[this.props.uid] = this;
|
2016-07-13 12:44:24 -08:00
|
|
|
}
|
|
|
|
|
|
2018-12-06 14:56:29 -09:00
|
|
|
onOpen() {
|
2018-03-17 04:51:36 -08:00
|
|
|
// we need to delay one frame so that styles
|
2017-08-02 11:05:47 -08:00
|
|
|
// get applied and we can make an accurate measurement
|
|
|
|
|
// of the container width and height
|
|
|
|
|
requestAnimationFrame(() => {
|
2017-09-24 02:01:55 -08:00
|
|
|
this.fitResize();
|
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-25 04:46:53 -08:00
|
|
|
console.warn(
|
|
|
|
|
'The underlying terminal engine of Hyper no longer ' +
|
|
|
|
|
'uses iframes with individual `document` objects for each ' +
|
|
|
|
|
'terminal instance. This method call is retained for ' +
|
2017-11-01 13:31:44 -08:00
|
|
|
"backwards compatibility reasons. It's ok to attach directly" +
|
2017-09-25 04:46:53 -08:00
|
|
|
'to the `document` object of the main `window`.'
|
|
|
|
|
);
|
|
|
|
|
return document;
|
2016-12-21 06:19:41 -09:00
|
|
|
}
|
2016-11-16 09:44:04 -09:00
|
|
|
|
2017-06-11 02:42:39 -08:00
|
|
|
onWindowResize() {
|
2017-09-24 02:01:55 -08:00
|
|
|
this.fitResize();
|
2016-10-03 18:00:50 -08:00
|
|
|
}
|
|
|
|
|
|
2017-10-22 13:16:52 -08:00
|
|
|
// intercepting paste event for any necessary processing of
|
|
|
|
|
// clipboard data, if result is falsy, paste event continues
|
|
|
|
|
onWindowPaste(e) {
|
|
|
|
|
if (!this.props.isTermActive) return;
|
|
|
|
|
|
|
|
|
|
const processed = processClipboard();
|
|
|
|
|
if (processed) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
this.term.send(processed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 14:01:21 -08:00
|
|
|
onMouseUp(e) {
|
|
|
|
|
if (this.props.quickEdit && e.button === 2) {
|
|
|
|
|
if (this.term.hasSelection()) {
|
|
|
|
|
clipboard.writeText(this.term.getSelection());
|
|
|
|
|
this.term.clearSelection();
|
|
|
|
|
} else {
|
|
|
|
|
document.execCommand('paste');
|
|
|
|
|
}
|
|
|
|
|
} else if (this.props.copyOnSelect && this.term.hasSelection()) {
|
2017-10-24 14:06:46 -08:00
|
|
|
clipboard.writeText(this.term.getSelection());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 02:21:16 -09:00
|
|
|
selectAll() {
|
|
|
|
|
this.term.selectAll();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
fitResize() {
|
2017-11-06 03:26:56 -09:00
|
|
|
if (!this.termWrapperRef) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-01-15 08:36:02 -09:00
|
|
|
this.term.fit();
|
2016-07-03 12:35:45 -08:00
|
|
|
}
|
|
|
|
|
|
2017-09-03 11:15:33 -08:00
|
|
|
keyboardHandler(e) {
|
2017-11-02 18:51:18 -08:00
|
|
|
// Has Mousetrap flagged this event as a command?
|
|
|
|
|
return !e.catched;
|
2017-09-03 11:15:33 -08:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
2018-01-09 07:33:24 -09:00
|
|
|
const nextTermOptions = getTermOptions(nextProps);
|
|
|
|
|
|
|
|
|
|
// Update only options that have changed.
|
|
|
|
|
Object.keys(nextTermOptions)
|
|
|
|
|
.filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
|
|
|
|
|
.forEach(option => this.term.setOption(option, nextTermOptions[option]));
|
|
|
|
|
|
|
|
|
|
// Do we need to update theme?
|
|
|
|
|
const shouldUpdateTheme =
|
|
|
|
|
!this.termOptions.theme ||
|
2018-03-22 11:59:07 -08:00
|
|
|
Object.keys(nextTermOptions.theme).some(
|
|
|
|
|
option => nextTermOptions.theme[option] !== this.termOptions.theme[option]
|
|
|
|
|
);
|
2018-01-09 07:33:24 -09:00
|
|
|
if (shouldUpdateTheme) {
|
|
|
|
|
this.term.setOption('theme', nextTermOptions.theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.termOptions = nextTermOptions;
|
2017-08-02 11:05:47 -08:00
|
|
|
|
2017-09-24 02:01:55 -08:00
|
|
|
if (!this.props.isTermActive && nextProps.isTermActive) {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.fitResize();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-20 14:22:34 -08:00
|
|
|
if (
|
|
|
|
|
this.props.fontSize !== nextProps.fontSize ||
|
|
|
|
|
this.props.fontFamily !== nextProps.fontFamily ||
|
2018-05-10 02:54:00 -08:00
|
|
|
this.props.lineHeight !== nextProps.lineHeight ||
|
|
|
|
|
this.props.letterSpacing !== nextProps.letterSpacing
|
2018-04-20 14:22:34 -08:00
|
|
|
) {
|
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`
|
2018-12-06 14:56:29 -09:00
|
|
|
this.disposableListeners.forEach(handler => handler.dispose());
|
|
|
|
|
this.disposableListeners = [];
|
2017-08-02 11:05:47 -08:00
|
|
|
|
|
|
|
|
window.removeEventListener('resize', this.onWindowResize, {
|
|
|
|
|
passive: true
|
2017-09-10 05:35:10 -08:00
|
|
|
});
|
2017-10-22 13:16:52 -08:00
|
|
|
|
|
|
|
|
window.removeEventListener('paste', this.onWindowPaste, {
|
|
|
|
|
capture: true
|
|
|
|
|
});
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2018-03-17 04:51:36 -08:00
|
|
|
render() {
|
2017-09-10 05:35:10 -08:00
|
|
|
return (
|
2017-10-24 14:06:46 -08:00
|
|
|
<div
|
2018-03-17 04:51:36 -08:00
|
|
|
className={`term_fit ${this.props.isTermActive ? 'term_active' : ''}`}
|
2017-10-24 14:06:46 -08:00
|
|
|
style={{padding: this.props.padding}}
|
|
|
|
|
onMouseUp={this.onMouseUp}
|
|
|
|
|
>
|
2017-09-10 05:35:10 -08:00
|
|
|
{this.props.customChildrenBefore}
|
2018-03-17 04:51:36 -08:00
|
|
|
<div ref={this.onTermWrapperRef} className="term_fit term_wrapper">
|
|
|
|
|
<div ref={this.onTermRef} className="term_fit term_term" />
|
2017-09-10 05:35:10 -08:00
|
|
|
</div>
|
|
|
|
|
{this.props.customChildren}
|
2018-03-17 04:51:36 -08:00
|
|
|
|
|
|
|
|
<style jsx>{`
|
|
|
|
|
.term_fit {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.term_wrapper {
|
|
|
|
|
/* TODO: decide whether to keep this or not based on understanding what xterm-selection is for */
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
`}</style>
|
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
|
|
|
}
|
|
|
|
|
}
|