hyper/lib/components/term.js

329 lines
9.1 KiB
JavaScript
Raw Normal View History

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';
import * as fit from 'xterm/lib/addons/fit/fit';
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';
import {PureComponent} from '../base-components';
import terms from '../terms';
import processClipboard from '../utils/paste';
2017-06-11 02:42:39 -08:00
Terminal.applyAddon(fit);
2017-06-11 02:42:39 -08:00
// map old hterm constants to xterm.js
const CURSOR_STYLES = {
BEAM: 'bar',
UNDERLINE: 'underline',
BLOCK: 'block'
};
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
const backgroundColor = Color(props.backgroundColor).alpha() < 1 ? 'transparent' : props.backgroundColor;
return {
macOptionIsMeta: props.modifierKeys.altIsMeta,
2018-01-09 07:33:24 -09:00
cursorStyle: CURSOR_STYLES[props.cursorShape],
cursorBlink: props.cursorBlink,
fontFamily: props.fontFamily,
fontSize: props.fontSize,
allowTransparency: true,
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
}
};
};
export default class Term extends PureComponent {
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);
this.termRef = null;
this.termWrapperRef = null;
this.termRect = null;
this.onOpen = this.onOpen.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onWindowPaste = this.onWindowPaste.bind(this);
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 = {};
2016-07-13 12:44:24 -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);
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
}
if (this.props.isTermActive) {
this.term.focus();
}
2018-01-09 07:33:24 -09:00
this.onOpen(this.termOptions);
2017-06-11 02:42:39 -08:00
if (props.onTitle) {
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) {
this.term.on('focus', props.onActive);
}
2017-06-11 02:42:39 -08:00
if (props.onData) {
this.term.on('data', props.onData);
}
2017-06-11 02:42:39 -08:00
if (props.onResize) {
this.term.on('resize', ({cols, rows}) => {
props.onResize(cols, rows);
});
}
2016-07-13 12:44:24 -08:00
if (props.onCursorMove) {
this.term.on('cursormove', () => {
const cursorFrame = {
x: this.term.buffer.x * this.term.renderer.dimensions.actualCellWidth,
y: this.term.buffer.y * this.term.renderer.dimensions.actualCellHeight,
width: this.term.renderer.dimensions.actualCellWidth,
height: this.term.renderer.dimensions.actualCellHeight,
col: this.term.buffer.y,
row: this.term.buffer.x
};
props.onCursorMove(cursorFrame);
});
}
2017-08-02 11:05:47 -08:00
window.addEventListener('resize', this.onWindowResize, {
passive: true
});
Split Panes (#693) * npm: add .npmrc with save-exact=true * split panes: create initial implementation This allows users to split their Hyperterm terms into multiple nested splits, both vertical and horizontal. Fixes #56 * split panes: suport closing tabs and individual panes * split panes: ensure new splits are placed at the correct index New split panes should be placed after the currently active pane, not at the end like they were previously. * split panes: add explicit dependency to uuid * split panes: implement split pane cycling This adds menu buttons for moving back and forward between open split panes in the currect terminal tab. Doesn't add a hotkey yet, needs some bikeshedding. * split panes: move activeSessionUid to its own object It made little sense to have so many objects with `activeSessionUid` set to `null` when it only mattered on the top level. Now it's an object mapping term-group `uid` to `sessionUid` instead. * split panes: make sure closing the last split pane exits the app * split panes: fix a crash after closing specific panes Sometimes the terminal would crash when a specific split pane was closed, because the `activeSessions` mapping wasn't updated correctly. * split panes: fix a bug that caused initial session sizing to be wrong * fix all our focus / blur issues in one fell swoop :O (famous last words) * get rid of react warning * hterm: make sure not to lose focus when VT listens on clicks * term: restore onactive callback * add missing `return` to override (just in case) * split pane: new split pane implementation * goodbye react-split-pane * added term group resizing action and reducer * terms: supply border color so that we can use it for splits * term-group: add resizing hook * term-groups: add resizing constant * remove split pane css side-effect * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split-pane: remove `console.log` * split-pane: remove `console.log` * split panes: rebalance sizes on insert/removal * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split panes: rebalance sizes on insert/removal * split panes: set a minimum size for resizing * split-pane: fix vertical splits * css :| * package: bump electron * split panes: attach onFocus listener to webviews * 1.4.1 and 1.4.2 are broken. they have the following regression: - open google.com on the main window - open a new tab - come back to previous tab. webview is gone :| * split panes: handle PTY exits * split panes: add linux friendly keybindings
2016-10-03 18:00:50 -08:00
window.addEventListener('paste', this.onWindowPaste, {
capture: true
});
terms[this.props.uid] = this;
2016-07-13 12:44:24 -08:00
}
2018-01-09 07:33:24 -09:00
onOpen(termOptions) {
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
2018-01-09 07:33:24 -09:00
// eslint-disable-next-line no-debugger
//debugger;
this.term.charMeasure.measure(termOptions);
this.fitResize();
});
2017-08-02 11:05:47 -08:00
}
getTermDocument() {
2017-06-11 02:42:39 -08:00
// eslint-disable-next-line no-console
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" +
'to the `document` object of the main `window`.'
);
return document;
}
2017-06-11 02:42:39 -08:00
onWindowResize() {
this.fitResize();
Split Panes (#693) * npm: add .npmrc with save-exact=true * split panes: create initial implementation This allows users to split their Hyperterm terms into multiple nested splits, both vertical and horizontal. Fixes #56 * split panes: suport closing tabs and individual panes * split panes: ensure new splits are placed at the correct index New split panes should be placed after the currently active pane, not at the end like they were previously. * split panes: add explicit dependency to uuid * split panes: implement split pane cycling This adds menu buttons for moving back and forward between open split panes in the currect terminal tab. Doesn't add a hotkey yet, needs some bikeshedding. * split panes: move activeSessionUid to its own object It made little sense to have so many objects with `activeSessionUid` set to `null` when it only mattered on the top level. Now it's an object mapping term-group `uid` to `sessionUid` instead. * split panes: make sure closing the last split pane exits the app * split panes: fix a crash after closing specific panes Sometimes the terminal would crash when a specific split pane was closed, because the `activeSessions` mapping wasn't updated correctly. * split panes: fix a bug that caused initial session sizing to be wrong * fix all our focus / blur issues in one fell swoop :O (famous last words) * get rid of react warning * hterm: make sure not to lose focus when VT listens on clicks * term: restore onactive callback * add missing `return` to override (just in case) * split pane: new split pane implementation * goodbye react-split-pane * added term group resizing action and reducer * terms: supply border color so that we can use it for splits * term-group: add resizing hook * term-groups: add resizing constant * remove split pane css side-effect * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split-pane: remove `console.log` * split-pane: remove `console.log` * split panes: rebalance sizes on insert/removal * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split panes: rebalance sizes on insert/removal * split panes: set a minimum size for resizing * split-pane: fix vertical splits * css :| * package: bump electron * split panes: attach onFocus listener to webviews * 1.4.1 and 1.4.2 are broken. they have the following regression: - open google.com on the main window - open a new tab - come back to previous tab. webview is gone :| * split panes: handle PTY exits * split panes: add linux friendly keybindings
2016-10-03 18:00:50 -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());
}
}
write(data) {
2017-06-11 02:42:39 -08:00
this.term.write(data);
2016-07-13 12:44:24 -08:00
}
focus() {
2017-06-11 02:42:39 -08:00
this.term.focus();
2016-07-13 12:44:24 -08:00
}
clear() {
2017-06-11 02:42:39 -08:00
this.term.clear();
2017-08-02 11:05:47 -08:00
}
reset() {
2017-08-02 11:05:47 -08:00
this.term.reset();
}
resize(cols, rows) {
2017-08-02 11:05:47 -08:00
this.term.resize(cols, rows);
}
selectAll() {
this.term.selectAll();
}
fitResize() {
2017-11-06 03:26:56 -09:00
if (!this.termWrapperRef) {
return;
}
this.term.fit();
2016-07-03 12:35:45 -08:00
}
keyboardHandler(e) {
// Has Mousetrap flagged this event as a command?
return !e.catched;
}
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 ||
Object.keys(nextTermOptions.theme).some(option => {
nextTermOptions.theme[option] !== this.termOptions.theme[option];
});
if (shouldUpdateTheme) {
this.term.setOption('theme', nextTermOptions.theme);
}
this.termOptions = nextTermOptions;
2017-08-02 11:05:47 -08:00
if (!this.props.isTermActive && nextProps.isTermActive) {
requestAnimationFrame(() => {
2018-01-09 07:33:24 -09:00
this.term.charMeasure.measure(this.termOptions);
this.fitResize();
});
}
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
2018-01-09 07:33:24 -09:00
this.term.charMeasure.measure(this.termOptions);
2017-08-02 11:05:47 -08:00
// resize to fit the container
this.fitResize();
2017-08-02 11:05:47 -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;
}
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
// 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`
['title', 'focus', 'data', 'resize', 'cursormove'].forEach(type => this.term.removeAllListeners(type));
2017-08-02 11:05:47 -08:00
window.removeEventListener('resize', this.onWindowResize, {
passive: true
});
window.removeEventListener('paste', this.onWindowPaste, {
capture: true
});
2016-06-30 22:01:04 -08:00
}
template(css) {
return (
2017-10-24 14:06:46 -08:00
<div
className={css('fit', this.props.isTermActive && 'active')}
style={{padding: this.props.padding}}
onMouseUp={this.onMouseUp}
>
{this.props.customChildrenBefore}
<div ref={this.onTermWrapperRef} className={css('fit', 'wrapper')}>
<div ref={this.onTermRef} className={css('fit', 'term')} />
</div>
{this.props.customChildren}
2017-08-02 11:05:47 -08:00
</div>
);
2016-06-30 22:01:04 -08:00
}
styles() {
2016-07-13 12:44:24 -08:00
return {
fit: {
Split Panes (#693) * npm: add .npmrc with save-exact=true * split panes: create initial implementation This allows users to split their Hyperterm terms into multiple nested splits, both vertical and horizontal. Fixes #56 * split panes: suport closing tabs and individual panes * split panes: ensure new splits are placed at the correct index New split panes should be placed after the currently active pane, not at the end like they were previously. * split panes: add explicit dependency to uuid * split panes: implement split pane cycling This adds menu buttons for moving back and forward between open split panes in the currect terminal tab. Doesn't add a hotkey yet, needs some bikeshedding. * split panes: move activeSessionUid to its own object It made little sense to have so many objects with `activeSessionUid` set to `null` when it only mattered on the top level. Now it's an object mapping term-group `uid` to `sessionUid` instead. * split panes: make sure closing the last split pane exits the app * split panes: fix a crash after closing specific panes Sometimes the terminal would crash when a specific split pane was closed, because the `activeSessions` mapping wasn't updated correctly. * split panes: fix a bug that caused initial session sizing to be wrong * fix all our focus / blur issues in one fell swoop :O (famous last words) * get rid of react warning * hterm: make sure not to lose focus when VT listens on clicks * term: restore onactive callback * add missing `return` to override (just in case) * split pane: new split pane implementation * goodbye react-split-pane * added term group resizing action and reducer * terms: supply border color so that we can use it for splits * term-group: add resizing hook * term-groups: add resizing constant * remove split pane css side-effect * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split-pane: remove `console.log` * split-pane: remove `console.log` * split panes: rebalance sizes on insert/removal * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split panes: rebalance sizes on insert/removal * split panes: set a minimum size for resizing * split-pane: fix vertical splits * css :| * package: bump electron * split panes: attach onFocus listener to webviews * 1.4.1 and 1.4.2 are broken. they have the following regression: - open google.com on the main window - open a new tab - come back to previous tab. webview is gone :| * split panes: handle PTY exits * split panes: add linux friendly keybindings
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
}
}