mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 04:28:41 -09:00
* [UI] Make scrollbar look the same on all platforms Linux suffers from the same issue as other platforms where the scrollbar is that ugly electron style. This commit will fix this and make it look the same on all platforms and cut down code. * linter : Use const instead of let * naming : make the constant easy to understand The previous code used a variable to define code specific to the os. Since we combined the code for all platforms it is not required anymore, so I changed the name to be more understandable to the person reading the code. * [UI] Make scrollbar look the same on all platforms Linux suffers from the same issue as other platforms where the scrollbar is that ugly electron style. This commit will fix this and make it look the same on all platforms and cut down code. * linter : Use const instead of let * naming : make the constant easy to understand The previous code used a variable to define code specific to the os. Since we combined the code for all platforms it is not required anymore, so I changed the name to be more understandable to the person reading the code.
418 lines
11 KiB
JavaScript
418 lines
11 KiB
JavaScript
/* global Blob,URL,requestAnimationFrame */
|
|
import React from 'react';
|
|
import Color from 'color';
|
|
import uuid from 'uuid';
|
|
import hterm from '../hterm';
|
|
import Component from '../component';
|
|
import getColorList from '../utils/colors';
|
|
import notify from '../utils/notify';
|
|
|
|
export default class Term extends Component {
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleWheel = this.handleWheel.bind(this);
|
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
this.handleScrollEnter = this.handleScrollEnter.bind(this);
|
|
this.handleScrollLeave = this.handleScrollLeave.bind(this);
|
|
this.onHyperCaret = this.onHyperCaret.bind(this);
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
this.handleFocus = this.handleFocus.bind(this);
|
|
props.ref_(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
const {props} = this;
|
|
this.term = props.term || new hterm.Terminal(uuid.v4());
|
|
this.term.onHyperCaret(this.hyperCaret);
|
|
|
|
// the first term that's created has unknown size
|
|
// subsequent new tabs have size
|
|
if (props.cols && props.rows) {
|
|
this.term.realizeSize_(props.cols, props.rows);
|
|
}
|
|
|
|
const prefs = this.term.getPrefs();
|
|
|
|
prefs.set('font-family', props.fontFamily);
|
|
prefs.set('font-size', props.fontSize);
|
|
prefs.set('font-smoothing', props.fontSmoothing);
|
|
prefs.set('cursor-color', this.validateColor(props.cursorColor, 'rgba(255,255,255,0.5)'));
|
|
prefs.set('enable-clipboard-notice', false);
|
|
prefs.set('foreground-color', props.foregroundColor);
|
|
|
|
// hterm.ScrollPort.prototype.setBackgroundColor is overriden
|
|
// to make hterm's background transparent. we still need to set
|
|
// background-color for proper text rendering
|
|
prefs.set('background-color', props.backgroundColor);
|
|
prefs.set('color-palette-overrides', getColorList(props.colors));
|
|
prefs.set('user-css', this.getStylesheet(props.customCSS));
|
|
prefs.set('scrollbar-visible', false);
|
|
prefs.set('receive-encoding', 'raw');
|
|
prefs.set('send-encoding', 'raw');
|
|
prefs.set('alt-sends-what', 'browser-key');
|
|
|
|
if (props.bell === 'SOUND') {
|
|
prefs.set('audible-bell-sound', this.props.bellSoundURL);
|
|
} else {
|
|
prefs.set('audible-bell-sound', '');
|
|
}
|
|
|
|
if (props.copyOnSelect) {
|
|
prefs.set('copy-on-select', true);
|
|
} else {
|
|
prefs.set('copy-on-select', false);
|
|
}
|
|
|
|
this.term.onTerminalReady = () => {
|
|
const io = this.term.io.push();
|
|
io.onVTKeystroke = io.sendString = props.onData;
|
|
io.onTerminalResize = (cols, rows) => {
|
|
if (cols !== this.props.cols || rows !== this.props.rows) {
|
|
props.onResize(cols, rows);
|
|
}
|
|
};
|
|
|
|
this.term.modifierKeys = props.modifierKeys;
|
|
// this.term.CursorNode_ is available at this point.
|
|
this.term.setCursorShape(props.cursorShape);
|
|
|
|
// emit onTitle event when hterm instance
|
|
// wants to set the title of its tab
|
|
this.term.setWindowTitle = props.onTitle;
|
|
this.term.focusHyperCaret();
|
|
};
|
|
this.term.decorate(this.termRef);
|
|
this.term.installKeyboard();
|
|
if (this.props.onTerminal) {
|
|
this.props.onTerminal(this.term);
|
|
}
|
|
|
|
const iframeWindow = this.getTermDocument().defaultView;
|
|
iframeWindow.addEventListener('wheel', this.handleWheel);
|
|
|
|
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
|
|
}
|
|
|
|
handleWheel(e) {
|
|
if (this.props.onWheel) {
|
|
this.props.onWheel(e);
|
|
}
|
|
const prefs = this.term.getPrefs();
|
|
prefs.set('scrollbar-visible', true);
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
if (!this.scrollMouseEnter) {
|
|
this.scrollbarsHideTimer = setTimeout(() => {
|
|
prefs.set('scrollbar-visible', false);
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
handleScrollEnter() {
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
const prefs = this.term.getPrefs();
|
|
prefs.set('scrollbar-visible', true);
|
|
this.scrollMouseEnter = true;
|
|
}
|
|
|
|
handleScrollLeave() {
|
|
const prefs = this.term.getPrefs();
|
|
prefs.set('scrollbar-visible', false);
|
|
this.scrollMouseEnter = false;
|
|
}
|
|
|
|
handleMouseUp() {
|
|
this.props.onActive();
|
|
// this makes sure that we focus the hyper caret only
|
|
// if a click on the term does not result in a selection
|
|
// otherwise, if we focus without such check, it'd be
|
|
// impossible to select a piece of text
|
|
if (this.term.document_.getSelection().type !== 'Range') {
|
|
this.term.focusHyperCaret();
|
|
}
|
|
}
|
|
handleFocus() {
|
|
// This will in turn result in `this.focus()` being
|
|
// called, which is unecessary.
|
|
// Should investigate if it matters.
|
|
this.props.onActive();
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
if (e.ctrlKey && e.key === 'c') {
|
|
this.props.onURLAbort();
|
|
}
|
|
}
|
|
|
|
onHyperCaret(caret) {
|
|
this.hyperCaret = caret;
|
|
}
|
|
|
|
write(data) {
|
|
// sometimes the preference set above for
|
|
// `receive-encoding` is not known by the vt
|
|
// before we type to write (since the preference
|
|
// manager is asynchronous), so we force it to
|
|
// avoid buffering
|
|
// this fixes a race condition where sometimes
|
|
// opening new sessions results in broken
|
|
// output due to the term attempting to decode
|
|
// as `utf-8` instead of `raw`
|
|
if (this.term.vt.characterEncoding !== 'raw') {
|
|
this.term.vt.characterEncoding = 'raw';
|
|
}
|
|
|
|
this.term.io.writeUTF8(data);
|
|
}
|
|
|
|
focus() {
|
|
this.term.focusHyperCaret();
|
|
}
|
|
|
|
clear() {
|
|
this.term.clearPreserveCursorRow();
|
|
|
|
// If cursor is still not at the top, a command is probably
|
|
// running and we'd like to delete the whole screen.
|
|
// Move cursor to top
|
|
if (this.term.getCursorRow() !== 0) {
|
|
this.write('\x1B[0;0H\x1B[2J');
|
|
}
|
|
}
|
|
|
|
moveWordLeft() {
|
|
this.term.onVTKeystroke('\x1bb');
|
|
}
|
|
|
|
moveWordRight() {
|
|
this.term.onVTKeystroke('\x1bf');
|
|
}
|
|
|
|
deleteWordLeft() {
|
|
this.term.onVTKeystroke('\x1b\x7f');
|
|
}
|
|
|
|
deleteWordRight() {
|
|
this.term.onVTKeystroke('\x1bd');
|
|
}
|
|
|
|
deleteLine() {
|
|
this.term.onVTKeystroke('\x1bw');
|
|
}
|
|
|
|
moveToStart() {
|
|
this.term.onVTKeystroke('\x01');
|
|
}
|
|
|
|
moveToEnd() {
|
|
this.term.onVTKeystroke('\x05');
|
|
}
|
|
|
|
selectAll() {
|
|
this.term.selectAll();
|
|
}
|
|
|
|
getScreenNode() {
|
|
return this.term.scrollPort_.getScreenNode();
|
|
}
|
|
|
|
getTermDocument() {
|
|
return this.term.document_;
|
|
}
|
|
|
|
getStylesheet(css) {
|
|
const hyperCaret = `
|
|
.hyper-caret {
|
|
outline: none;
|
|
display: inline-block;
|
|
color: transparent;
|
|
text-shadow: 0 0 0 black;
|
|
font-family: ${this.props.fontFamily};
|
|
font-size: ${this.props.fontSize}px;
|
|
}
|
|
`;
|
|
const scrollBarCss = `
|
|
::-webkit-scrollbar {
|
|
width: 5px;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
-webkit-border-radius: 10px;
|
|
border-radius: 10px;
|
|
background: ${this.props.borderColor};
|
|
}
|
|
::-webkit-scrollbar-thumb:window-inactive {
|
|
background: ${this.props.borderColor};
|
|
}
|
|
`;
|
|
return URL.createObjectURL(new Blob([`
|
|
.cursor-node[focus="false"] {
|
|
border-width: 1px !important;
|
|
}
|
|
.cursor-node[focus="true"] {
|
|
border-left-width: 1px;
|
|
border-bottom-width: 2px;
|
|
}
|
|
${hyperCaret}
|
|
${scrollBarCss}
|
|
${css}
|
|
`], {type: 'text/css'}));
|
|
}
|
|
|
|
validateColor(color, alternative = 'rgb(255,255,255)') {
|
|
try {
|
|
return Color(color).rgbString();
|
|
} catch (err) {
|
|
notify(`color "${color}" is invalid`);
|
|
}
|
|
return alternative;
|
|
}
|
|
|
|
handleMouseDown(ev) {
|
|
// we prevent losing focus when clicking the boundary
|
|
// wrappers of the main terminal element
|
|
if (ev.target === this.termWrapperRef ||
|
|
ev.target === this.termRef) {
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (this.props.url !== nextProps.url) {
|
|
// when the url prop changes, we make sure
|
|
// the terminal starts or stops ignoring
|
|
// key input so that it doesn't conflict
|
|
// with the <webview>
|
|
if (nextProps.url) {
|
|
this.term.io.push();
|
|
window.addEventListener('keydown', this.handleKeyDown);
|
|
} else {
|
|
window.removeEventListener('keydown', this.handleKeyDown);
|
|
this.term.io.pop();
|
|
}
|
|
}
|
|
|
|
if (!this.props.cleared && nextProps.cleared) {
|
|
this.clear();
|
|
}
|
|
|
|
const prefs = this.term.getPrefs();
|
|
|
|
if (this.props.fontSize !== nextProps.fontSize) {
|
|
prefs.set('font-size', nextProps.fontSize);
|
|
this.hyperCaret.style.fontSize = nextProps.fontSize + 'px';
|
|
}
|
|
|
|
if (this.props.foregroundColor !== nextProps.foregroundColor) {
|
|
prefs.set('foreground-color', nextProps.foregroundColor);
|
|
}
|
|
|
|
if (this.props.fontFamily !== nextProps.fontFamily) {
|
|
prefs.set('font-family', nextProps.fontFamily);
|
|
this.hyperCaret.style.fontFamily = nextProps.fontFamily;
|
|
}
|
|
|
|
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
|
|
prefs.set('font-smoothing', nextProps.fontSmoothing);
|
|
}
|
|
|
|
if (this.props.cursorColor !== nextProps.cursorColor) {
|
|
prefs.set('cursor-color', this.validateColor(nextProps.cursorColor, 'rgba(255,255,255,0.5)'));
|
|
}
|
|
|
|
if (this.props.cursorShape !== nextProps.cursorShape) {
|
|
this.term.setCursorShape(nextProps.cursorShape);
|
|
}
|
|
|
|
if (this.props.colors !== nextProps.colors) {
|
|
prefs.set('color-palette-overrides', getColorList(nextProps.colors));
|
|
}
|
|
|
|
if (this.props.customCSS !== nextProps.customCSS) {
|
|
prefs.set('user-css', this.getStylesheet(nextProps.customCSS));
|
|
}
|
|
|
|
if (this.props.bell === 'SOUND') {
|
|
prefs.set('audible-bell-sound', this.props.bellSoundURL);
|
|
} else {
|
|
prefs.set('audible-bell-sound', '');
|
|
}
|
|
|
|
if (this.props.copyOnSelect) {
|
|
prefs.set('copy-on-select', true);
|
|
} else {
|
|
prefs.set('copy-on-select', false);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
this.props.ref_(null);
|
|
}
|
|
|
|
template(css) {
|
|
return (<div
|
|
ref={component => {
|
|
this.termWrapperRef = component;
|
|
}}
|
|
className={css('fit', this.props.isTermActive && 'active')}
|
|
onMouseDown={this.handleMouseDown}
|
|
style={{padding: this.props.padding}}
|
|
>
|
|
{ this.props.customChildrenBefore }
|
|
<div
|
|
ref={component => {
|
|
this.termRef = component;
|
|
}}
|
|
className={css('fit', 'term')}
|
|
/>
|
|
{ this.props.url ?
|
|
<webview
|
|
key="hyper-webview"
|
|
src={this.props.url}
|
|
onFocus={this.handleFocus}
|
|
style={{
|
|
background: '#fff',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
display: 'inline-flex',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}
|
|
/> :
|
|
<div // eslint-disable-line react/jsx-indent
|
|
key="scrollbar"
|
|
className={css('scrollbarShim')}
|
|
onMouseEnter={this.handleScrollEnter}
|
|
onMouseLeave={this.handleScrollLeave}
|
|
/>
|
|
}
|
|
<div key="hyper-caret" contentEditable className="hyper-caret" ref={this.onHyperCaret}/>
|
|
{ this.props.customChildren }
|
|
</div>);
|
|
}
|
|
|
|
styles() {
|
|
return {
|
|
fit: {
|
|
display: 'block',
|
|
width: '100%',
|
|
height: '100%'
|
|
},
|
|
|
|
term: {
|
|
position: 'relative'
|
|
},
|
|
|
|
scrollbarShim: {
|
|
position: 'fixed',
|
|
right: 0,
|
|
width: '50px',
|
|
top: 0,
|
|
bottom: 0,
|
|
pointerEvents: 'none'
|
|
}
|
|
};
|
|
}
|
|
}
|