mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
* Dynamically change the `font-smoothing` pref By default, hterm defaults to `font-smoothing: 'antialiased'`, which works really well on retina displays. On non-retina displays, however, the type looks very thin and is hard to read. This will look at the devicePixelRatio of the device anytime the term prefs are set, and change between `antialiased` and `subpixel-antialiased` dynamically. * Refactor to add the font smoothing override into state This also subscribes to the electron `move` event to control when this piece of state gets updated. * Add UI_WINDOW_MOVE action with a side effect for font smoothing
247 lines
6.5 KiB
JavaScript
247 lines
6.5 KiB
JavaScript
/* global Blob,URL,requestAnimationFrame */
|
|
import React from 'react';
|
|
import hterm from '../hterm';
|
|
import Component from '../component';
|
|
|
|
export default class Term extends Component {
|
|
|
|
constructor (props) {
|
|
super(props);
|
|
this.onWheel = this.onWheel.bind(this);
|
|
this.onScrollEnter = this.onScrollEnter.bind(this);
|
|
this.onScrollLeave = this.onScrollLeave.bind(this);
|
|
props.ref_(this);
|
|
}
|
|
|
|
componentDidMount () {
|
|
const { props } = this;
|
|
this.term = new hterm.Terminal();
|
|
|
|
// 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);
|
|
}
|
|
|
|
this.term.prefs_.set('font-family', props.fontFamily);
|
|
this.term.prefs_.set('font-size', props.fontSize);
|
|
this.term.prefs_.set('font-smoothing', props.fontSmoothing);
|
|
this.term.prefs_.set('cursor-color', props.cursorColor);
|
|
this.term.prefs_.set('enable-clipboard-notice', false);
|
|
this.term.prefs_.set('foreground-color', props.foregroundColor);
|
|
this.term.prefs_.set('background-color', props.backgroundColor);
|
|
this.term.prefs_.set('color-palette-overrides', props.colors);
|
|
this.term.prefs_.set('user-css', this.getStylesheet(props.customCSS));
|
|
this.term.prefs_.set('scrollbar-visible', false);
|
|
this.term.prefs_.set('receive-encoding', 'raw');
|
|
this.term.prefs_.set('send-encoding', 'raw');
|
|
this.term.prefs_.set('alt-sends-what', 'browser-key');
|
|
|
|
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.decorate(this.refs.term);
|
|
this.term.installKeyboard();
|
|
if (this.props.onTerminal) this.props.onTerminal(this.term);
|
|
|
|
const iframeWindow = this.getTermDocument().defaultView;
|
|
iframeWindow.addEventListener('wheel', this.onWheel);
|
|
}
|
|
|
|
onWheel () {
|
|
this.term.prefs_.set('scrollbar-visible', true);
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
if (!this.scrollMouseEnter) {
|
|
this.scrollbarsHideTimer = setTimeout(() => {
|
|
this.term.prefs_.set('scrollbar-visible', false);
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
onScrollEnter () {
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
this.term.prefs_.set('scrollbar-visible', true);
|
|
this.scrollMouseEnter = true;
|
|
}
|
|
|
|
onScrollLeave () {
|
|
this.term.prefs_.set('scrollbar-visible', false);
|
|
this.scrollMouseEnter = false;
|
|
}
|
|
|
|
write (data) {
|
|
requestAnimationFrame(() => {
|
|
this.term.io.writeUTF8(data);
|
|
});
|
|
}
|
|
|
|
focus () {
|
|
this.term.focus();
|
|
}
|
|
|
|
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.term.io.writeUTF8('\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');
|
|
}
|
|
|
|
getTermDocument () {
|
|
return this.term.document_;
|
|
}
|
|
|
|
getStylesheet (css) {
|
|
const blob = new Blob([`
|
|
.cursor-node[focus="false"] {
|
|
border-width: 1px !important;
|
|
}
|
|
${css}
|
|
`]);
|
|
return URL.createObjectURL(blob, { type: 'text/css' });
|
|
}
|
|
|
|
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) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
if (!this.props.cleared && nextProps.cleared) {
|
|
this.clear();
|
|
}
|
|
|
|
if (this.props.fontSize !== nextProps.fontSize) {
|
|
this.term.prefs_.set('font-size', nextProps.fontSize);
|
|
}
|
|
|
|
if (this.props.foregroundColor !== nextProps.foregroundColor) {
|
|
this.term.prefs_.set('foreground-color', nextProps.foregroundColor);
|
|
}
|
|
|
|
if (this.props.backgroundColor !== nextProps.backgroundColor) {
|
|
this.term.prefs_.set('background-color', nextProps.backgroundColor);
|
|
}
|
|
|
|
if (this.props.fontFamily !== nextProps.fontFamily) {
|
|
this.term.prefs_.set('font-family', nextProps.fontFamily);
|
|
}
|
|
|
|
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
|
|
this.term.prefs_.set('font-smoothing', props.fontSmoothing);
|
|
}
|
|
|
|
if (this.props.cursorColor !== nextProps.cursorColor) {
|
|
this.term.prefs_.set('cursor-color', nextProps.cursorColor);
|
|
}
|
|
|
|
if (this.props.colors !== nextProps.colors) {
|
|
this.term.prefs_.set('color-palette-overrides', nextProps.colors);
|
|
}
|
|
|
|
if (this.props.customCSS !== nextProps.customCSS) {
|
|
this.term.prefs_.set('user-css', this.getStylesheet(nextProps.customCSS));
|
|
}
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
clearTimeout(this.scrollbarsHideTimer);
|
|
this.props.ref_(null);
|
|
}
|
|
|
|
template (css) {
|
|
return <div className={ css('fit') }>
|
|
{ this.props.customChildrenBefore }
|
|
<div ref='term' className={ css('fit', 'term') } />
|
|
{ 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>
|
|
: <div
|
|
className={ css('scrollbarShim') }
|
|
onMouseEnter={ this.onScrollEnter }
|
|
onMouseLeave={ this.onScrollLeave } />
|
|
}
|
|
{ this.props.customChildren }
|
|
</div>;
|
|
}
|
|
|
|
styles () {
|
|
return {
|
|
fit: {
|
|
width: '100%',
|
|
height: '100%'
|
|
},
|
|
|
|
term: {
|
|
position: 'relative'
|
|
},
|
|
|
|
scrollbarShim: {
|
|
position: 'fixed',
|
|
right: 0,
|
|
width: '50px',
|
|
top: 0,
|
|
bottom: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
}
|