bunch of changes

This commit is contained in:
Guillermo Rauch 2017-06-11 13:42:39 +03:00
parent ca849425ac
commit 6a1dcf9ef0
7 changed files with 2941 additions and 672 deletions

6
.eslintignore Normal file
View file

@ -0,0 +1,6 @@
build
app/dist
app/static
assets
website
node_modules

File diff suppressed because it is too large Load diff

View file

@ -1,368 +1,103 @@
/* global Blob,URL,requestAnimationFrame */ /* global Blob,URL,requestAnimationFrame */
import React from 'react'; import React from 'react';
import Color from 'color';
import uuid from 'uuid';
import hterm from '../hterm';
import Component from '../component'; import Component from '../component';
import getColorList from '../utils/colors';
import terms from '../terms'; import terms from '../terms';
import notify from '../utils/notify'; import Terminal from 'hyper-xterm-tmp';
// map old hterm constants to xterm.js
const CURSOR_STYLES = {
BEAM: 'bar',
UNDERLINE: 'underline',
BLOCK: 'block'
}
export default class Term extends Component { export default class Term extends Component {
constructor(props) { constructor(props) {
super(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); props.ref_(this);
this.termRef = null
this.onWindowResize = this.onWindowResize.bind(this)
} }
componentDidMount() { componentDidMount() {
const {props} = this; 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 this.term = props.term || new Terminal({
// subsequent new tabs have size cursorStyle: CURSOR_STYLES[props.cursorShape],
if (props.cols && props.rows) { cursorBlink: props.cursorBlink,
this.term.realizeSize_(props.cols, props.rows); cols: props.cols,
rows: props.rows
});
this.term.open(this.termRef)
if (props.onTitle) {
this.term.on(
'title',
props.onTitle
)
} }
const prefs = this.term.getPrefs(); if (props.onActive) {
this.term.on(
prefs.set('font-family', props.fontFamily); 'focus',
prefs.set('font-size', props.fontSize); props.onTitle
prefs.set('font-smoothing', props.fontSmoothing); )
prefs.set('cursor-color', this.validateColor(props.cursorColor, 'rgba(255,255,255,0.5)'));
prefs.set('cursor-blink', props.cursorBlink);
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) { if (props.onData) {
prefs.set('copy-on-select', true); this.term.on(
} else { 'data',
prefs.set('copy-on-select', false); props.onData
)
} }
this.term.onTerminalReady = () => { if (props.onResize) {
const io = this.term.io.push(); this.term.on(
io.onVTKeystroke = io.sendString = props.onData; 'resize',
io.onTerminalResize = (cols, rows) => { ({ cols, rows }) => {
if (cols !== this.props.cols || rows !== this.props.rows) { props.onResize(cols, rows)
props.onResize(cols, rows);
} }
}; )
this.term.modifierKeys = props.modifierKeys;
// this.term.CursorNode_ is available at this point.
this.term.setCursorShape(props.cursorShape);
// required to be set for CursorBlink to work
this.term.setCursorVisible(true);
// 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; window.addEventListener('resize', this.onWindowResize)
iframeWindow.addEventListener('wheel', this.handleWheel);
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
this.getScreenNode().addEventListener('mousedown', this.handleMouseDown);
terms[this.props.uid] = this; terms[this.props.uid] = this;
} }
handleWheel(e) { getTermDocument () {
if (this.props.onWheel) { // eslint-disable-next-line no-console
this.props.onWheel(e); console.error('unimplemented')
}
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() { onWindowResize() {
clearTimeout(this.scrollbarsHideTimer); // eslint-disable-next-line no-console
const prefs = this.term.getPrefs(); console.error('unimplemented')
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) { write(data) {
// sometimes the preference set above for this.term.write(data);
// `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() { focus() {
this.term.focusHyperCaret(); this.term.focus();
} }
clear() { clear() {
this.term.wipeContents(); this.term.clear();
this.term.onVTKeystroke('\f'); this.term.onVTKeystroke('\f');
} }
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};
}
`;
const selectCss = `
::selection {
background: ${Color(this.props.cursorColor).alpha(0.4).rgbString()};
}
`;
return URL.createObjectURL(new Blob([`
.cursor-node[focus="false"] {
border-width: 1px !important;
}
x-row {
line-height: 1.2em;
}
${hyperCaret}
${scrollBarCss}
${selectCss}
${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();
}
if (this.props.quickEdit) {
this.term.onMouseDown_(ev);
}
}
componentWillReceiveProps(nextProps) { 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) { if (!this.props.cleared && nextProps.cleared) {
this.clear(); 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.cursorBlink !== nextProps.cursorBlink) {
prefs.set('cursor-blink', nextProps.cursorBlink);
}
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() { componentWillUnmount() {
terms[this.props.uid] = this; terms[this.props.uid] = this;
// turn blinking off to prevent leaking a timeout when disposing terminal
const prefs = this.term.getPrefs();
prefs.set('cursor-blink', false);
clearTimeout(this.scrollbarsHideTimer);
this.props.ref_(null); this.props.ref_(null);
} }
@ -372,7 +107,6 @@ export default class Term extends Component {
this.termWrapperRef = component; this.termWrapperRef = component;
}} }}
className={css('fit', this.props.isTermActive && 'active')} className={css('fit', this.props.isTermActive && 'active')}
onMouseDown={this.handleMouseDown}
style={{padding: this.props.padding}} style={{padding: this.props.padding}}
> >
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
@ -382,29 +116,6 @@ export default class Term extends Component {
}} }}
className={css('fit', 'term')} 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 } { this.props.customChildren }
</div>); </div>);
} }
@ -417,18 +128,7 @@ export default class Term extends Component {
height: '100%' height: '100%'
}, },
term: { term: {}
position: 'relative'
},
scrollbarShim: {
position: 'fixed',
right: 0,
width: '50px',
top: 0,
bottom: 0,
pointerEvents: 'none'
}
}; };
} }
} }

View file

@ -3,8 +3,11 @@ import Component from '../component';
import {decorate, getTermGroupProps} from '../utils/plugins'; import {decorate, getTermGroupProps} from '../utils/plugins';
import CommandRegistry from '../command-registry'; import CommandRegistry from '../command-registry';
import TermGroup_ from './term-group'; import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet';
const TermGroup = decorate(TermGroup_, 'TermGroup'); const TermGroup = decorate(TermGroup_, 'TermGroup');
const StyleSheet = decorate(StyleSheet_, 'StyleSheet');
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends Component { export default class Terms extends Component {
@ -81,17 +84,10 @@ export default class Terms extends Component {
terms: this.terms, terms: this.terms,
activeSession: this.props.activeSession, activeSession: this.props.activeSession,
sessions: this.props.sessions, sessions: this.props.sessions,
customCSS: this.props.customCSS,
fontSize: this.props.fontSize,
borderColor: this.props.borderColor, borderColor: this.props.borderColor,
cursorColor: this.props.cursorColor,
cursorShape: this.props.cursorShape, cursorShape: this.props.cursorShape,
cursorBlink: this.props.cursorBlink, cursorBlink: this.props.cursorBlink,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily, uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
padding: this.props.padding, padding: this.props.padding,
colors: this.props.colors, colors: this.props.colors,
bell: this.props.bell, bell: this.props.bell,
@ -122,6 +118,15 @@ export default class Terms extends Component {
}) })
} }
{ this.props.customChildren } { this.props.customChildren }
<StyleSheet
customCSS={this.props.customCSS}
cursorColor={this.props.cursorColor}
fontSize={this.props.fontSize}
fontFamily={this.props.fontFamily}
fontSmoothing={this.props.fontSmoothing}
foregroundColor={this.props.foregroundColor}
backgroundColor={this.props.backgroundColor}
/>
</div>); </div>);
} }

View file

@ -5,7 +5,7 @@
"app": "electron app", "app": "electron app",
"dev": "webpack -w", "dev": "webpack -w",
"build": "cross-env NODE_ENV=production webpack", "build": "cross-env NODE_ENV=production webpack",
"lint": "xo", "lint": "eslint .",
"test": "npm run lint", "test": "npm run lint",
"test:unit": "ava test/unit", "test:unit": "ava test/unit",
"test:unit:watch": "npm run test:unit -- --watch", "test:unit:watch": "npm run test:unit -- --watch",
@ -15,28 +15,42 @@
"dist": "npm run build && cross-env BABEL_ENV=production babel --out-file app/dist/bundle.js --no-comments --minified app/dist/bundle.js && build", "dist": "npm run build && cross-env BABEL_ENV=production babel --out-file app/dist/bundle.js --no-comments --minified app/dist/bundle.js && build",
"clean": "npm cache clear && rm -rf node_modules && rm -rf app/node_modules && rm -rf app/dist" "clean": "npm cache clear && rm -rf node_modules && rm -rf app/node_modules && rm -rf app/dist"
}, },
"xo": { "eslintConfig": {
"extends": "xo-react", "plugins": [
"esnext": true, "react"
"space": true,
"env": [
"browser",
"node"
], ],
"rules": { "extends": [
"new-cap": 0, "eslint:recommended",
"complexity": 0, "plugin:react/recommended"
"react/prop-types": 0, ],
"react/jsx-no-bind": 0, "parserOptions": {
"linebreak-style": 0 "ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true,
"experimentalObjectRestSpread": true
},
"allowImportExportEverywhere": true
}, },
"ignores": [ "env": {
"build/**", "es6": true,
"app/dist/**", "browser": true,
"app/static/**", "node": true
"assets/**", },
"website/**" "rules": {
] "func-names": [
"error",
"as-needed"
],
"no-shadow": "error",
"no-extra-semi": 0,
"react/prop-types": 0,
"react/react-in-jsx-scope": 0,
"react/no-unescaped-entities": 0,
"react/jsx-no-target-blank": 0,
"react/no-string-refs": 0
}
}, },
"babel": { "babel": {
"presets": [ "presets": [
@ -116,6 +130,7 @@
"dependencies": { "dependencies": {
"aphrodite-simple": "0.4.1", "aphrodite-simple": "0.4.1",
"color": "0.11.4", "color": "0.11.4",
"css-loader": "^0.28.4",
"hterm-umdjs": "1.1.3", "hterm-umdjs": "1.1.3",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"mousetrap": "1.6.1", "mousetrap": "1.6.1",
@ -131,7 +146,9 @@
"runes": "0.4.0", "runes": "0.4.0",
"seamless-immutable": "6.1.3", "seamless-immutable": "6.1.3",
"semver": "5.3.0", "semver": "5.3.0",
"uuid": "3.0.1" "style-loader": "^0.18.2",
"uuid": "3.0.1",
"hyper-xterm-tmp": "2.8.0"
}, },
"devDependencies": { "devDependencies": {
"asar": "0.13.0", "asar": "0.13.0",
@ -148,12 +165,11 @@
"electron-builder-squirrel-windows": "18.3.0", "electron-builder-squirrel-windows": "18.3.0",
"electron-devtools-installer": "2.2.0", "electron-devtools-installer": "2.2.0",
"electron-rebuild": "1.5.11", "electron-rebuild": "1.5.11",
"eslint-config-xo-react": "0.10.0", "eslint": "^3.19.0",
"eslint-plugin-react": "6.7.1", "eslint-plugin-react": "^7.0.1",
"husky": "0.13.4", "husky": "0.13.4",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"spectron": "3.6.4", "spectron": "3.6.4",
"webpack": "2.6.1", "webpack": "2.6.1"
"xo": "0.17.1"
} }
} }

View file

@ -23,6 +23,11 @@ module.exports = {
{ {
test: /\.json/, test: /\.json/,
loader: 'json-loader' loader: 'json-loader'
},
// for xterm.js
{
test: /\.css$/,
loader: 'style-loader!css-loader'
} }
] ]
}, },

875
yarn.lock

File diff suppressed because it is too large Load diff