mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
Implement hterm (#28)
* remove legacy css * hyperterm: delegate rows / cols calculation to hterm * session: handle pty kill problems * index: fix memory leak by removing sessions from the map upon exit * app: remove local copy of `xterm.js` * term: implement the `hterm` API and some needed overrides * package: add `hterm-umd` * hyperterm: add optimistic tab exit * hyperterm: delegate key combination detection to the hterm <iframe> document * term: register keyboard * session: fix incorrect width after resizing and creating a new tab (#13) * tabs: fix `user-select` css property * term: fix focus issue when exiting a url Instead of uninstalling the keyboard, we keep the focus on the underlying terminal. We register a new IO handler so that we intercept all data events. The reason we need to do this is that we can't programmatically restore focus on the underlying terminal unless it's in the same tick as a user event (ie: click). Since we were uninstalling the keyboard and subsequently attempting to reinstall it without such an event, pressing Ctrl+C after a url was effectively resulting in a loss of focus and a horrible horrible experience. Now it's fixed :) * text-metrics: remove module no longer used hterm has a much better calculation technique anyways * term: fix default bg * term: fix nasty hterm bug that triggered an infinite copy loop * index: add separator in `View` menu for full screen item * term: implement cmd+K clearing and improve hterm's `wipeContents`
This commit is contained in:
parent
c0394e7764
commit
d8e841a3d8
9 changed files with 230 additions and 4938 deletions
|
|
@ -35,11 +35,6 @@ header {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terms div {
|
|
||||||
font-family: Menlo;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.term {
|
.term {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ nav {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
color: #9B9B9B;
|
color: #9B9B9B;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single {
|
.single {
|
||||||
|
|
|
||||||
110
app/hyperterm.js
110
app/hyperterm.js
|
|
@ -3,7 +3,6 @@ import Term from './term';
|
||||||
import RPC from './rpc';
|
import RPC from './rpc';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import classes from 'classnames';
|
import classes from 'classnames';
|
||||||
import getTextMetrics from './text-metrics';
|
|
||||||
import shallowCompare from 'react-addons-shallow-compare';
|
import shallowCompare from 'react-addons-shallow-compare';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import UpdateChecker from './update-checker';
|
import UpdateChecker from './update-checker';
|
||||||
|
|
@ -12,6 +11,8 @@ export default class HyperTerm extends Component {
|
||||||
constructor () {
|
constructor () {
|
||||||
super();
|
super();
|
||||||
this.state = {
|
this.state = {
|
||||||
|
cols: null,
|
||||||
|
rows: null,
|
||||||
hpadding: 10,
|
hpadding: 10,
|
||||||
vpadding: 5,
|
vpadding: 5,
|
||||||
sessions: [],
|
sessions: [],
|
||||||
|
|
@ -38,6 +39,7 @@ export default class HyperTerm extends Component {
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.openExternal = this.openExternal.bind(this);
|
this.openExternal = this.openExternal.bind(this);
|
||||||
this.focusActive = this.focusActive.bind(this);
|
this.focusActive = this.focusActive.bind(this);
|
||||||
|
this.closeBrowser = this.closeBrowser.bind(this);
|
||||||
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
|
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
|
||||||
|
|
||||||
this.moveLeft = this.moveLeft.bind(this);
|
this.moveLeft = this.moveLeft.bind(this);
|
||||||
|
|
@ -65,16 +67,18 @@ export default class HyperTerm extends Component {
|
||||||
ref='termWrapper'>{
|
ref='termWrapper'>{
|
||||||
this.state.sessions.map((uid, i) => {
|
this.state.sessions.map((uid, i) => {
|
||||||
const active = i === this.state.active;
|
const active = i === this.state.active;
|
||||||
return <div key={`d${uid}`} className={classes('term', { active })} ref='term'>
|
return <div key={`d${uid}`} className={classes('term', { active })} style={{ width: '100%', height: '100%' }} ref='term'>
|
||||||
<Term
|
<Term
|
||||||
key={uid}
|
key={uid}
|
||||||
ref={`term-${uid}`}
|
ref={`term-${uid}`}
|
||||||
url={this.state.urls[uid]}
|
|
||||||
cols={this.state.cols}
|
cols={this.state.cols}
|
||||||
rows={this.state.rows}
|
rows={this.state.rows}
|
||||||
|
url={this.state.urls[uid]}
|
||||||
|
onResize={this.onResize}
|
||||||
onTitle={this.setTitle.bind(this, uid)}
|
onTitle={this.setTitle.bind(this, uid)}
|
||||||
onData={this.write.bind(this, uid)}
|
onData={this.write.bind(this, uid)}
|
||||||
onURL={this.onURL.bind(this, uid)}
|
onURL={this.onURL.bind(this, uid)}
|
||||||
|
onURLAbort={this.closeBrowser}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
|
|
@ -97,13 +101,15 @@ export default class HyperTerm extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestTab () {
|
requestTab () {
|
||||||
this.rpc.emit('new', this.getDimensions());
|
// we send the hterm default size
|
||||||
|
this.rpc.emit('new', { cols: this.state.cols, rows: this.state.rows });
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTab () {
|
closeTab () {
|
||||||
if (this.state.sessions.length) {
|
if (this.state.sessions.length) {
|
||||||
const uid = this.state.sessions[this.state.active];
|
const uid = this.state.sessions[this.state.active];
|
||||||
this.rpc.emit('exit', { uid });
|
this.rpc.emit('exit', { uid });
|
||||||
|
this.onSessionExit(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,11 +163,16 @@ export default class HyperTerm extends Component {
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
if (this.state.active !== nextState.active) {
|
if (this.state.active !== nextState.active) {
|
||||||
const curUid = this.state.sessions[this.state.active];
|
const curUid = this.state.sessions[this.state.active];
|
||||||
if (curUid) {
|
// make sure that the blurred uid has not been
|
||||||
|
// optimistically removed
|
||||||
|
if (curUid && ~nextState.sessions.indexOf(curUid)) {
|
||||||
this.rpc.emit('blur', { uid: curUid });
|
this.rpc.emit('blur', { uid: curUid });
|
||||||
}
|
}
|
||||||
const nextUid = nextState.sessions[nextState.active];
|
const nextUid = nextState.sessions[nextState.active];
|
||||||
this.rpc.emit('focus', { uid: nextUid });
|
this.rpc.emit('focus', { uid: nextUid });
|
||||||
|
this.shouldInitKeys = true;
|
||||||
|
} else {
|
||||||
|
this.shouldInitKeys = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shallowCompare(this, nextProps, nextState);
|
return shallowCompare(this, nextProps, nextState);
|
||||||
|
|
@ -170,7 +181,6 @@ export default class HyperTerm extends Component {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.rpc = new RPC();
|
this.rpc = new RPC();
|
||||||
this.updateChecker = new UpdateChecker(this.onUpdateAvailable.bind(this));
|
this.updateChecker = new UpdateChecker(this.onUpdateAvailable.bind(this));
|
||||||
this.setState(this.getDimensions());
|
|
||||||
|
|
||||||
// open a new tab upon mounting
|
// open a new tab upon mounting
|
||||||
this.rpc.once('ready', () => this.requestTab());
|
this.rpc.once('ready', () => this.requestTab());
|
||||||
|
|
@ -188,6 +198,7 @@ export default class HyperTerm extends Component {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.rpc.on('clear', this.clearCurrentTerm.bind(this));
|
||||||
this.rpc.on('exit', this.onSessionExit.bind(this));
|
this.rpc.on('exit', this.onSessionExit.bind(this));
|
||||||
|
|
||||||
this.rpc.on('data', ({ uid, data }) => {
|
this.rpc.on('data', ({ uid, data }) => {
|
||||||
|
|
@ -210,31 +221,14 @@ export default class HyperTerm extends Component {
|
||||||
this.rpc.on('close tab', this.closeTab.bind(this));
|
this.rpc.on('close tab', this.closeTab.bind(this));
|
||||||
this.rpc.on('title', this.onRemoteTitle.bind(this));
|
this.rpc.on('title', this.onRemoteTitle.bind(this));
|
||||||
|
|
||||||
window.addEventListener('resize', this.onResize);
|
|
||||||
|
|
||||||
this.rpc.on('move left', this.moveLeft);
|
this.rpc.on('move left', this.moveLeft);
|
||||||
this.rpc.on('move right', this.moveRight);
|
this.rpc.on('move right', this.moveRight);
|
||||||
|
}
|
||||||
|
|
||||||
Mousetrap.bind('command+1', this.moveTo.bind(this, 0));
|
clearCurrentTerm () {
|
||||||
Mousetrap.bind('command+2', this.moveTo.bind(this, 1));
|
const uid = this.state.sessions[this.state.active];
|
||||||
Mousetrap.bind('command+3', this.moveTo.bind(this, 2));
|
const term = this.refs[`term-${uid}`];
|
||||||
Mousetrap.bind('command+4', this.moveTo.bind(this, 3));
|
term.clear();
|
||||||
Mousetrap.bind('command+5', this.moveTo.bind(this, 4));
|
|
||||||
Mousetrap.bind('command+6', this.moveTo.bind(this, 5));
|
|
||||||
Mousetrap.bind('command+7', this.moveTo.bind(this, 6));
|
|
||||||
Mousetrap.bind('command+8', this.moveTo.bind(this, 7));
|
|
||||||
Mousetrap.bind('command+9', this.moveTo.bind(this, 8));
|
|
||||||
|
|
||||||
Mousetrap.bind('ctrl+c', this.closeBrowser.bind(this));
|
|
||||||
|
|
||||||
Mousetrap.bind('command+shift+left', this.moveLeft);
|
|
||||||
Mousetrap.bind('command+shift+right', this.moveRight);
|
|
||||||
|
|
||||||
Mousetrap.bind('command+shift+[', this.moveLeft);
|
|
||||||
Mousetrap.bind('command+shift+]', this.moveRight);
|
|
||||||
|
|
||||||
Mousetrap.bind('command+alt+left', this.moveLeft);
|
|
||||||
Mousetrap.bind('command+alt+right', this.moveRight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateAvailable (updateVersion) {
|
onUpdateAvailable (updateVersion) {
|
||||||
|
|
@ -268,6 +262,11 @@ export default class HyperTerm extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSessionExit ({ uid }) {
|
onSessionExit ({ uid }) {
|
||||||
|
if (!~this.state.sessions.indexOf(uid)) {
|
||||||
|
console.log('ignore exit of', uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessions: _sessions,
|
sessions: _sessions,
|
||||||
titles: _titles,
|
titles: _titles,
|
||||||
|
|
@ -317,6 +316,33 @@ export default class HyperTerm extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
|
if (this.shouldInitKeys) {
|
||||||
|
if (this.keys) {
|
||||||
|
this.keys.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = this.state.sessions[this.state.active];
|
||||||
|
const term = this.refs[`term-${uid}`];
|
||||||
|
const keys = new Mousetrap(term.getTermDocument());
|
||||||
|
keys.bind('command+1', this.moveTo.bind(this, 0));
|
||||||
|
keys.bind('command+2', this.moveTo.bind(this, 1));
|
||||||
|
keys.bind('command+3', this.moveTo.bind(this, 2));
|
||||||
|
keys.bind('command+4', this.moveTo.bind(this, 3));
|
||||||
|
keys.bind('command+5', this.moveTo.bind(this, 4));
|
||||||
|
keys.bind('command+6', this.moveTo.bind(this, 5));
|
||||||
|
keys.bind('command+7', this.moveTo.bind(this, 6));
|
||||||
|
keys.bind('command+8', this.moveTo.bind(this, 7));
|
||||||
|
keys.bind('command+9', this.moveTo.bind(this, 8));
|
||||||
|
keys.bind('command+shift+left', this.moveLeft);
|
||||||
|
keys.bind('command+shift+right', this.moveRight);
|
||||||
|
keys.bind('command+shift+[', this.moveLeft);
|
||||||
|
keys.bind('command+shift+]', this.moveRight);
|
||||||
|
keys.bind('command+alt+left', this.moveLeft);
|
||||||
|
keys.bind('command+alt+right', this.moveRight);
|
||||||
|
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
this.focusActive();
|
this.focusActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,13 +355,15 @@ export default class HyperTerm extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize () {
|
onResize (dim) {
|
||||||
const dim = this.getDimensions();
|
|
||||||
if (dim.rows !== this.state.rows || dim.cols !== this.state.cols) {
|
if (dim.rows !== this.state.rows || dim.cols !== this.state.cols) {
|
||||||
this.ignoreActivity = Date.now();
|
this.ignoreActivity = Date.now();
|
||||||
|
|
||||||
this.rpc.emit('resize', dim);
|
this.rpc.emit('resize', dim);
|
||||||
const state = Object.assign({}, dim, { resizeIndicatorShowing: true });
|
const state = Object.assign({}, dim,
|
||||||
|
// if it's the first time we hear about the resize we
|
||||||
|
// don't show the indicator
|
||||||
|
null === this.state.rows ? {} : { resizeIndicatorShowing: true }
|
||||||
|
);
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
clearTimeout(this.resizeIndicatorTimeout);
|
clearTimeout(this.resizeIndicatorTimeout);
|
||||||
this.resizeIndicatorTimeout = setTimeout(() => {
|
this.resizeIndicatorTimeout = setTimeout(() => {
|
||||||
|
|
@ -370,24 +398,12 @@ export default class HyperTerm extends Component {
|
||||||
this.headerMouseDownWindowY = window.screenY;
|
this.headerMouseDownWindowY = window.screenY;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDimensions () {
|
|
||||||
const tm = getTextMetrics('Menlo', '11px', '15px');
|
|
||||||
const hp = this.state.hpadding;
|
|
||||||
const vp = this.state.vpadding;
|
|
||||||
const el = this.refs.termWrapper;
|
|
||||||
const { width, height } = el.getBoundingClientRect();
|
|
||||||
const dim = {
|
|
||||||
cols: Math.floor((width - hp * 2) / tm.width),
|
|
||||||
rows: Math.floor((height - vp * 2) / tm.height)
|
|
||||||
};
|
|
||||||
return dim;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
window.removeEventListener('resize', this.onResize);
|
|
||||||
this.rpc.destroy();
|
this.rpc.destroy();
|
||||||
clearTimeout(this.resizeIndicatorTimeout);
|
clearTimeout(this.resizeIndicatorTimeout);
|
||||||
Mousetrap.reset();
|
if (this.keys) {
|
||||||
|
this.keys.reset();
|
||||||
|
}
|
||||||
this.updateChecker.destroy();
|
this.updateChecker.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-addons-shallow-compare": "15.1.0",
|
|
||||||
"mousetrap": "1.6.0",
|
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
|
"hterm-umd": "1.0.1",
|
||||||
|
"json-loader": "0.5.4",
|
||||||
|
"mousetrap": "1.6.0",
|
||||||
"react": "15.1.0",
|
"react": "15.1.0",
|
||||||
|
"react-addons-shallow-compare": "15.1.0",
|
||||||
"react-dom": "15.1.0",
|
"react-dom": "15.1.0",
|
||||||
"semver-compare": "^1.0.0",
|
"semver-compare": "1.0.0"
|
||||||
"json-loader": "^0.5.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "2.13.1",
|
"eslint": "2.13.1",
|
||||||
|
|
|
||||||
162
app/term.js
162
app/term.js
|
|
@ -1,38 +1,107 @@
|
||||||
import Terminal from './xterm';
|
/*global URL:false,Blob:false*/
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { hterm, lib as htermLib } from 'hterm-umd';
|
||||||
|
|
||||||
|
hterm.defaultStorage = new htermLib.Storage.Memory();
|
||||||
|
|
||||||
|
// override double click behavior to copy
|
||||||
|
const oldMouse = hterm.Terminal.prototype.onMouse_;
|
||||||
|
hterm.Terminal.prototype.onMouse_ = function (e) {
|
||||||
|
if ('dblclick' === e.type) {
|
||||||
|
console.log('[hyperterm+hterm] ignore double click');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return oldMouse.call(this, e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// there's no option to turn off the size overlay
|
||||||
|
hterm.Terminal.prototype.overlaySize = function () {};
|
||||||
|
|
||||||
|
// fixing a bug in hterm where a double click triggers
|
||||||
|
// a non-collapsed selection whose text is '', and results
|
||||||
|
// in an infinite copy loop
|
||||||
|
hterm.Terminal.prototype.copySelectionToClipboard = function () {
|
||||||
|
var text = this.getSelectionText();
|
||||||
|
if (text != null && text !== '') {
|
||||||
|
this.copyStringToClipboard(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// passthrough all the commands that are meant to control
|
||||||
|
// hyperterm and not the terminal itself
|
||||||
|
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
|
||||||
|
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
|
||||||
|
if (e.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return oldKeyDown.call(this, e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldKeyPress = hterm.Keyboard.prototype.onKeyPress_;
|
||||||
|
hterm.Keyboard.prototype.onKeyPress_ = function (e) {
|
||||||
|
if (e.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return oldKeyPress.call(this, e);
|
||||||
|
};
|
||||||
|
|
||||||
const domainRegex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/;
|
const domainRegex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/;
|
||||||
|
|
||||||
export default class Term extends Component {
|
export default class Term extends Component {
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.term = new Terminal({
|
this.term = new hterm.Terminal();
|
||||||
cols: this.props.cols,
|
|
||||||
rows: this.props.rows
|
// the first term that's created has unknown size
|
||||||
});
|
// subsequent new tabs have size
|
||||||
this.term.on('data', (data) => {
|
if (this.props.cols) {
|
||||||
this.props.onData(data);
|
this.term.realizeSize_(this.props.cols, this.props.rows);
|
||||||
});
|
}
|
||||||
this.term.on('title', (title) => {
|
|
||||||
this.props.onTitle(title);
|
this.term.prefs_.set('font-family', 'Menlo');
|
||||||
});
|
this.term.prefs_.set('font-size', 11);
|
||||||
this.term.open(this.refs.term);
|
this.term.prefs_.set('cursor-color', '#F81CE5');
|
||||||
|
this.term.prefs_.set('enable-clipboard-notice', false);
|
||||||
|
this.term.prefs_.set('background-color', '#000');
|
||||||
|
|
||||||
|
this.term.prefs_.set('user-css', URL.createObjectURL(new Blob([`
|
||||||
|
.cursor-node[focus="false"] {
|
||||||
|
border-width: 1px !important;
|
||||||
|
}
|
||||||
|
`]), { type: 'text/css' }));
|
||||||
|
|
||||||
|
this.term.onTerminalReady = () => {
|
||||||
|
const io = this.term.io.push();
|
||||||
|
io.onVTKeystroke = io.sendString = (str) => {
|
||||||
|
this.props.onData(str);
|
||||||
|
};
|
||||||
|
io.onTerminalResize = (cols, rows) => {
|
||||||
|
this.props.onResize({ cols, rows });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
this.term.decorate(this.refs.term);
|
||||||
|
this.term.installKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTermDocument () {
|
||||||
|
return this.term.document_;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps) {
|
shouldComponentUpdate (nextProps) {
|
||||||
if (nextProps.rows !== this.props.rows || nextProps.cols !== this.props.cols) {
|
|
||||||
this.term.resize(nextProps.cols, nextProps.rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.url !== nextProps.url) {
|
if (this.props.url !== nextProps.url) {
|
||||||
// when the url prop changes, we make sure
|
// when the url prop changes, we make sure
|
||||||
// the terminal starts or stops ignoring
|
// the terminal starts or stops ignoring
|
||||||
// key input so that it doesn't conflict
|
// key input so that it doesn't conflict
|
||||||
// with the <webview>
|
// with the <webview>
|
||||||
if (nextProps.url) {
|
if (nextProps.url) {
|
||||||
this.term.ignoreKeyEvents = true;
|
const io = this.term.io.push();
|
||||||
|
io.onVTKeystroke = io.sendString = (str) => {
|
||||||
|
if (1 === str.length && 3 === str.charCodeAt(0) /* Ctrl + C */) {
|
||||||
|
this.props.onURLAbort();
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
this.term.ignoreKeyEvents = false;
|
this.term.io.pop();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -51,20 +120,67 @@ export default class Term extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.term.write(data);
|
this.term.io.print(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
this.term.element.focus();
|
this.term.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
const { term } = this;
|
||||||
|
|
||||||
|
// we re-implement `wipeContents` to preserve the line
|
||||||
|
// and cursor position that the client is in.
|
||||||
|
// otherwise the user ends up with a completely clear
|
||||||
|
// screen which is really strange
|
||||||
|
term.scrollbackRows_.length = 0;
|
||||||
|
term.scrollPort_.resetCache();
|
||||||
|
|
||||||
|
[term.primaryScreen_, term.alternateScreen_].forEach(function (screen) {
|
||||||
|
const bottom = screen.getHeight();
|
||||||
|
if (bottom > 0) {
|
||||||
|
term.renumberRows_(0, bottom);
|
||||||
|
|
||||||
|
const x = screen.cursorPosition.column;
|
||||||
|
const y = screen.cursorPosition.row;
|
||||||
|
|
||||||
|
if (y === 0) {
|
||||||
|
// Empty screen, nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < y; i++) {
|
||||||
|
screen.setCursorPosition(i, 0);
|
||||||
|
screen.clearCursorRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we move the row that the user was focused on
|
||||||
|
// to the top of the screen
|
||||||
|
term.moveRows_(y, 1, 0);
|
||||||
|
|
||||||
|
// we restore the cursor position
|
||||||
|
screen.setCursorPosition(0, x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
term.syncCursorPosition_();
|
||||||
|
term.scrollPort_.invalidate();
|
||||||
|
|
||||||
|
// this will avoid a bug where the `wipeContents`
|
||||||
|
// hterm API doens't send the scroll to the top
|
||||||
|
this.term.scrollPort_.redraw_();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.term.destroy();
|
// there's no need to manually destroy
|
||||||
|
// as all the events are attached to the iframe
|
||||||
|
// which gets removed
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return <div>
|
return <div style={{ width: '100%', height: '100%' }}>
|
||||||
<div ref='term' />
|
<div ref='term' style={{ position: 'relative', width: '100%', height: '100%' }} />
|
||||||
{ this.props.url
|
{ this.props.url
|
||||||
? <webview
|
? <webview
|
||||||
src={this.props.url}
|
src={this.props.url}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
const mem = new Map();
|
|
||||||
export default function getTextMetrics (family, fontSize, lineHeight) {
|
|
||||||
const id = family + '#' + fontSize + '#' + lineHeight;
|
|
||||||
const memd = mem.get(id);
|
|
||||||
if (memd) return memd;
|
|
||||||
const el = document.createElement('span');
|
|
||||||
const style = el.style;
|
|
||||||
style.display = 'inline-block';
|
|
||||||
style.fontFamily = family;
|
|
||||||
style.fontSize = fontSize;
|
|
||||||
style.lineHeight = lineHeight;
|
|
||||||
el.innerText = 'X';
|
|
||||||
document.body.appendChild(el);
|
|
||||||
const { width, height } = el.getBoundingClientRect();
|
|
||||||
const ret = { width, height };
|
|
||||||
document.body.removeChild(el);
|
|
||||||
mem.set(id, ret);
|
|
||||||
console.log('text metrics calculated for', family, fontSize, lineHeight, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
4834
app/xterm.js
4834
app/xterm.js
File diff suppressed because it is too large
Load diff
16
index.js
16
index.js
|
|
@ -68,6 +68,7 @@ app.on('ready', () => {
|
||||||
|
|
||||||
session.on('exit', () => {
|
session.on('exit', () => {
|
||||||
rpc.emit('exit', { uid });
|
rpc.emit('exit', { uid });
|
||||||
|
sessions.delete(uid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -184,7 +185,17 @@ app.on('ready', () => {
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
|
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
|
||||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
|
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
|
||||||
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }
|
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Clear',
|
||||||
|
accelerator: 'CmdOrCtrl+K',
|
||||||
|
click (item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('clear');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -206,6 +217,9 @@ app.on('ready', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
role: 'togglefullscreen'
|
role: 'togglefullscreen'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
session.js
10
session.js
|
|
@ -7,10 +7,10 @@ const TITLE_POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
module.exports = class Session extends EventEmitter {
|
module.exports = class Session extends EventEmitter {
|
||||||
|
|
||||||
constructor ({ rows, cols }) {
|
constructor ({ rows, cols: columns }) {
|
||||||
super();
|
super();
|
||||||
this.pty = spawn(defaultShell, ['--login'], {
|
this.pty = spawn(defaultShell, ['--login'], {
|
||||||
cols,
|
columns,
|
||||||
rows,
|
rows,
|
||||||
cwd: process.env.HOME,
|
cwd: process.env.HOME,
|
||||||
env: Object.assign({}, process.env, {
|
env: Object.assign({}, process.env, {
|
||||||
|
|
@ -88,7 +88,11 @@ module.exports = class Session extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy () {
|
||||||
this.pty.kill('SIGHUP');
|
try {
|
||||||
|
this.pty.kill('SIGHUP');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('exit error', err.stack);
|
||||||
|
}
|
||||||
this.emit('exit');
|
this.emit('exit');
|
||||||
this.ended = true;
|
this.ended = true;
|
||||||
clearTimeout(this.titlePoll);
|
clearTimeout(this.titlePoll);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue