mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 04:28:41 -09:00
213 lines
5.7 KiB
JavaScript
213 lines
5.7 KiB
JavaScript
/*global URL:false,Blob:false*/
|
|
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/;
|
|
|
|
export default class Term extends Component {
|
|
|
|
componentDidMount () {
|
|
this.term = new hterm.Terminal();
|
|
|
|
// the first term that's created has unknown size
|
|
// subsequent new tabs have size
|
|
if (this.props.cols) {
|
|
this.term.realizeSize_(this.props.cols, this.props.rows);
|
|
}
|
|
|
|
this.term.prefs_.set('font-family', "Menlo, 'DejaVu Sans Mono', 'Lucida Console', monospace");
|
|
this.term.prefs_.set('font-size', 11);
|
|
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) {
|
|
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) {
|
|
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 {
|
|
this.term.io.pop();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
write (data) {
|
|
const match = data.match(/bash: ((https?:\/\/)|(\/\/))?(.*): ((command not found)|(No such file or directory))/);
|
|
if (match) {
|
|
const url = match[4];
|
|
// extract the domain portion from the url
|
|
const domain = url.split('/')[0];
|
|
if (domainRegex.test(domain)) {
|
|
this.props.onURL(toURL(url));
|
|
return;
|
|
}
|
|
}
|
|
this.term.io.print(data);
|
|
}
|
|
|
|
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 (x === 0) {
|
|
// Empty screen, nothing to do.
|
|
return;
|
|
}
|
|
|
|
// here we move the row that the user was focused on
|
|
// to the top of the screen
|
|
term.moveRows_(y, 1, 0);
|
|
|
|
for (let i = 1; i < bottom; i++) {
|
|
screen.setCursorPosition(i, 0);
|
|
screen.clearCursorRow();
|
|
}
|
|
|
|
// 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 () {
|
|
// there's no need to manually destroy
|
|
// as all the events are attached to the iframe
|
|
// which gets removed
|
|
}
|
|
|
|
render () {
|
|
return <div style={{ width: '100%', height: '100%' }}>
|
|
<div ref='term' style={{ position: 'relative', width: '100%', height: '100%' }} />
|
|
{ this.props.url
|
|
? <webview
|
|
src={this.props.url}
|
|
style={{
|
|
background: '#000',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
display: 'inline-flex',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}></webview>
|
|
: null
|
|
}
|
|
</div>;
|
|
}
|
|
|
|
}
|
|
|
|
function toURL (domain) {
|
|
if (/^https?:\/\//.test(domain)) {
|
|
return domain;
|
|
}
|
|
|
|
if ('//' === domain.substr(0, 2)) {
|
|
return domain;
|
|
}
|
|
|
|
return 'http://' + domain;
|
|
}
|