mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-17 05:58:41 -09:00
latest
This commit is contained in:
parent
6a1dcf9ef0
commit
b302962d44
6 changed files with 156 additions and 73 deletions
|
|
@ -14,6 +14,7 @@ class TermGroup_ extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.bound = new WeakMap();
|
this.bound = new WeakMap();
|
||||||
|
this.termRefs = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
bind(fn, thisObj, uid) {
|
bind(fn, thisObj, uid) {
|
||||||
|
|
@ -50,19 +51,13 @@ class TermGroup_ extends Component {
|
||||||
const props = getTermProps(uid, this.props, {
|
const props = getTermProps(uid, this.props, {
|
||||||
isTermActive: uid === this.props.activeSession,
|
isTermActive: uid === this.props.activeSession,
|
||||||
term: termRef ? termRef.term : null,
|
term: termRef ? termRef.term : null,
|
||||||
customCSS: this.props.customCSS,
|
|
||||||
fontSize: this.props.fontSize,
|
|
||||||
cursorColor: this.props.cursorColor,
|
|
||||||
cursorShape: this.props.cursorShape,
|
|
||||||
cursorBlink: this.props.cursorBlink,
|
cursorBlink: this.props.cursorBlink,
|
||||||
|
fontSize: this.props.fontSize,
|
||||||
fontFamily: this.props.fontFamily,
|
fontFamily: this.props.fontFamily,
|
||||||
uiFontFamily: this.props.uiFontFamily,
|
uiFontFamily: this.props.uiFontFamily,
|
||||||
fontSmoothing: this.props.fontSmoothing,
|
fontSmoothing: this.props.fontSmoothing,
|
||||||
foregroundColor: this.props.foregroundColor,
|
|
||||||
backgroundColor: this.props.backgroundColor,
|
|
||||||
modifierKeys: this.props.modifierKeys,
|
modifierKeys: this.props.modifierKeys,
|
||||||
padding: this.props.padding,
|
padding: this.props.padding,
|
||||||
colors: this.props.colors,
|
|
||||||
url: session.url,
|
url: session.url,
|
||||||
cleared: session.cleared,
|
cleared: session.cleared,
|
||||||
cols: session.cols,
|
cols: session.cols,
|
||||||
|
|
@ -84,12 +79,34 @@ class TermGroup_ extends Component {
|
||||||
// which is inefficient. Should maybe do something similar
|
// which is inefficient. Should maybe do something similar
|
||||||
// to this.bind.
|
// to this.bind.
|
||||||
return (<Term
|
return (<Term
|
||||||
ref_={term => this.props.ref_(uid, term)}
|
ref_={term => {
|
||||||
|
if (term) {
|
||||||
|
this.termRefs[uid] = term
|
||||||
|
} else {
|
||||||
|
delete this.termRefs[uid]
|
||||||
|
}
|
||||||
|
this.props.ref_(uid, term)
|
||||||
|
}}
|
||||||
key={uid}
|
key={uid}
|
||||||
{...props}
|
{...props}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.props.termGroup.sizes != nextProps.termGroup.sizes) {
|
||||||
|
// whenever we change the ratio, we want to force a resize
|
||||||
|
// on all the splits. technically, we could have done this
|
||||||
|
// at the reducer level, which would be a better place for
|
||||||
|
// it, but this is not too inelegant considering that terms
|
||||||
|
// are already reporting their own size when, for example,
|
||||||
|
// the window gets resized
|
||||||
|
for (const uid in this.termRefs) {
|
||||||
|
const term = this.termRefs[uid]
|
||||||
|
term.measureResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template() {
|
template() {
|
||||||
const {childGroups, termGroup} = this.props;
|
const {childGroups, termGroup} = this.props;
|
||||||
if (termGroup.sessionUid) {
|
if (termGroup.sessionUid) {
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,32 @@ export default class Term extends Component {
|
||||||
super(props);
|
super(props);
|
||||||
props.ref_(this);
|
props.ref_(this);
|
||||||
this.termRef = null
|
this.termRef = null
|
||||||
|
this.termWrapperRef = null
|
||||||
|
this.termRect = null
|
||||||
|
this.onOpen = this.onOpen.bind(this)
|
||||||
this.onWindowResize = this.onWindowResize.bind(this)
|
this.onWindowResize = this.onWindowResize.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
|
|
||||||
this.term = props.term || new Terminal({
|
// we need to use this hack to retain the term reference
|
||||||
cursorStyle: CURSOR_STYLES[props.cursorShape],
|
// as we move the term around splits, until xterm adds
|
||||||
cursorBlink: props.cursorBlink,
|
// support for getState / setState
|
||||||
cols: props.cols,
|
if (props.term) {
|
||||||
rows: props.rows
|
this.term = props.term
|
||||||
});
|
this.termRef.appendChild(this.term.element)
|
||||||
|
this.onOpen()
|
||||||
this.term.open(this.termRef)
|
} else {
|
||||||
|
this.term = props.term || new Terminal({
|
||||||
|
cursorStyle: CURSOR_STYLES[props.cursorShape],
|
||||||
|
cursorBlink: props.cursorBlink
|
||||||
|
});
|
||||||
|
this.term.on('open', this.onOpen)
|
||||||
|
this.term.open(this.termRef, {
|
||||||
|
focus: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (props.onTitle) {
|
if (props.onTitle) {
|
||||||
this.term.on(
|
this.term.on(
|
||||||
|
|
@ -42,7 +54,7 @@ export default class Term extends Component {
|
||||||
if (props.onActive) {
|
if (props.onActive) {
|
||||||
this.term.on(
|
this.term.on(
|
||||||
'focus',
|
'focus',
|
||||||
props.onTitle
|
props.onActive
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,19 +74,49 @@ export default class Term extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize, {
|
||||||
|
passive: true
|
||||||
|
})
|
||||||
|
|
||||||
terms[this.props.uid] = this;
|
terms[this.props.uid] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpen () {
|
||||||
|
// we need to delay one frame so that aphrodite styles
|
||||||
|
// get applied and we can make an accurate measurement
|
||||||
|
// of the container width and height
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// at this point it would make sense for character
|
||||||
|
// measurement to have taken place but it seems that
|
||||||
|
// xterm.js might be doing this asynchronously, so
|
||||||
|
// we force it instead
|
||||||
|
this.term.charMeasure.measure();
|
||||||
|
this.measureResize();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getTermDocument () {
|
getTermDocument () {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('unimplemented')
|
console.error('unimplemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// measures the container and makes the decision
|
||||||
|
// whether to resize the term to fit the container
|
||||||
|
measureResize () {
|
||||||
|
console.log('performing measure resize')
|
||||||
|
const termRect = this.termWrapperRef.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (!this.termRect ||
|
||||||
|
termRect.width !== this.termRect.width ||
|
||||||
|
termRect.height !== this.termRect.height) {
|
||||||
|
this.termRect = termRect;
|
||||||
|
console.log('performing fit resize')
|
||||||
|
this.fitResize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onWindowResize() {
|
onWindowResize() {
|
||||||
// eslint-disable-next-line no-console
|
this.measureResize();
|
||||||
console.error('unimplemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write(data) {
|
write(data) {
|
||||||
|
|
@ -87,35 +129,84 @@ export default class Term extends Component {
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.term.clear();
|
this.term.clear();
|
||||||
this.term.onVTKeystroke('\f');
|
}
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
this.term.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
resize (cols, rows) {
|
||||||
|
this.term.resize(cols, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitResize () {
|
||||||
|
const cols = Math.floor(
|
||||||
|
this.termRect.width / this.term.charMeasure.width
|
||||||
|
)
|
||||||
|
const rows = Math.floor(
|
||||||
|
this.termRect.height / this.term.charMeasure.height
|
||||||
|
)
|
||||||
|
|
||||||
|
if (cols !== this.props.cols || rows !== this.props.rows) {
|
||||||
|
this.resize(cols, rows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (!this.props.cleared && nextProps.cleared) {
|
if (!this.props.cleared && nextProps.cleared) {
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.fontSize !== nextProps.fontSize ||
|
||||||
|
this.props.fontFamily !== nextProps.fontFamily) {
|
||||||
|
// invalidate xterm cache about how wide each
|
||||||
|
// character is
|
||||||
|
this.term.charMeasure.measure()
|
||||||
|
|
||||||
|
// resize to fit the container
|
||||||
|
this.fitResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.rows !== this.props.rows ||
|
||||||
|
nextProps.cols !== this.props.cols) {
|
||||||
|
this.resize(nextProps.cols, nextProps.rows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
terms[this.props.uid] = this;
|
terms[this.props.uid] = this;
|
||||||
this.props.ref_(null);
|
this.props.ref_(null);
|
||||||
|
|
||||||
|
// to clean up the terminal, we remove the listeners
|
||||||
|
// instead of invoking `destroy`, since it will make the
|
||||||
|
// term insta un-attachable in the future (which we need
|
||||||
|
// to do in case of splitting, see `componentDidMount`
|
||||||
|
this.term._events = {}
|
||||||
|
|
||||||
|
window.removeEventListener('resize', this.onWindowResize, {
|
||||||
|
passive: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
return (<div
|
return (<div
|
||||||
ref={component => {
|
|
||||||
this.termWrapperRef = component;
|
|
||||||
}}
|
|
||||||
className={css('fit', this.props.isTermActive && 'active')}
|
className={css('fit', this.props.isTermActive && 'active')}
|
||||||
style={{padding: this.props.padding}}
|
style={{padding: this.props.padding}}
|
||||||
>
|
>
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
<div
|
<div
|
||||||
ref={component => {
|
ref={component => {
|
||||||
this.termRef = component;
|
this.termWrapperRef = component;
|
||||||
}}
|
}}
|
||||||
className={css('fit', 'term')}
|
className={css('fit', 'wrapper')}
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
ref={component => {
|
||||||
|
this.termRef = component;
|
||||||
|
}}
|
||||||
|
className={css('term')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{ this.props.customChildren }
|
{ this.props.customChildren }
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +218,11 @@ export default class Term extends Component {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: '100%'
|
||||||
},
|
},
|
||||||
|
wrapper: {
|
||||||
|
// TODO: decide whether to keep this or not based on
|
||||||
|
// understanding what xterm-selection is for
|
||||||
|
overflow: 'hidden'
|
||||||
|
},
|
||||||
term: {}
|
term: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,10 @@ export default class Terms extends Component {
|
||||||
borderColor: this.props.borderColor,
|
borderColor: this.props.borderColor,
|
||||||
cursorShape: this.props.cursorShape,
|
cursorShape: this.props.cursorShape,
|
||||||
cursorBlink: this.props.cursorBlink,
|
cursorBlink: this.props.cursorBlink,
|
||||||
|
fontSize: this.props.fontSize,
|
||||||
|
fontFamily: this.props.fontFamily,
|
||||||
uiFontFamily: this.props.uiFontFamily,
|
uiFontFamily: this.props.uiFontFamily,
|
||||||
padding: this.props.padding,
|
padding: this.props.padding,
|
||||||
colors: this.props.colors,
|
|
||||||
bell: this.props.bell,
|
bell: this.props.bell,
|
||||||
bellSoundURL: this.props.bellSoundURL,
|
bellSoundURL: this.props.bellSoundURL,
|
||||||
copyOnSelect: this.props.copyOnSelect,
|
copyOnSelect: this.props.copyOnSelect,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable react/no-danger */
|
/* eslint-disable react/no-danger */
|
||||||
|
|
||||||
import Mousetrap from 'mousetrap';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Component from '../component';
|
import Component from '../component';
|
||||||
|
|
@ -36,43 +35,8 @@ class Hyper extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachKeyListeners() {
|
attachKeyListeners() {
|
||||||
const {moveTo, moveLeft, moveRight} = this.props;
|
// eslint-disable-next-line no-console
|
||||||
const term = this.terms.getActiveTerm();
|
console.error('removed key listeners')
|
||||||
if (!term) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastIndex = this.terms.getLastTermIndex();
|
|
||||||
const document = term.getTermDocument();
|
|
||||||
const keys = new Mousetrap(document);
|
|
||||||
keys.bind('mod+1', moveTo.bind(this, 0));
|
|
||||||
keys.bind('mod+2', moveTo.bind(this, 1));
|
|
||||||
keys.bind('mod+3', moveTo.bind(this, 2));
|
|
||||||
keys.bind('mod+4', moveTo.bind(this, 3));
|
|
||||||
keys.bind('mod+5', moveTo.bind(this, 4));
|
|
||||||
keys.bind('mod+6', moveTo.bind(this, 5));
|
|
||||||
keys.bind('mod+7', moveTo.bind(this, 6));
|
|
||||||
keys.bind('mod+8', moveTo.bind(this, 7));
|
|
||||||
keys.bind('mod+9', moveTo.bind(this, lastIndex));
|
|
||||||
|
|
||||||
keys.bind('mod+shift+left', moveLeft);
|
|
||||||
keys.bind('mod+shift+right', moveRight);
|
|
||||||
keys.bind('mod+shift+[', moveLeft);
|
|
||||||
keys.bind('mod+shift+]', moveRight);
|
|
||||||
keys.bind('mod+alt+left', moveLeft);
|
|
||||||
keys.bind('mod+alt+right', moveRight);
|
|
||||||
keys.bind('ctrl+shift+tab', moveLeft);
|
|
||||||
keys.bind('ctrl+tab', moveRight);
|
|
||||||
|
|
||||||
const bound = method => term[method].bind(term);
|
|
||||||
keys.bind('alt+left', bound('moveWordLeft'));
|
|
||||||
keys.bind('alt+right', bound('moveWordRight'));
|
|
||||||
keys.bind('alt+backspace', bound('deleteWordLeft'));
|
|
||||||
keys.bind('alt+del', bound('deleteWordRight'));
|
|
||||||
keys.bind('mod+backspace', bound('deleteLine'));
|
|
||||||
keys.bind('mod+left', bound('moveToStart'));
|
|
||||||
keys.bind('mod+right', bound('moveToEnd'));
|
|
||||||
keys.bind('mod+a', bound('selectAll'));
|
|
||||||
this.keys = keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTermsRef(terms) {
|
onTermsRef(terms) {
|
||||||
|
|
@ -97,14 +61,14 @@ class Hyper extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
const {isMac, customCSS, uiFontFamily, borderColor, maximized} = this.props;
|
const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized} = this.props;
|
||||||
const borderWidth = isMac ? '' :
|
const borderWidth = isMac_ ? '' :
|
||||||
`${maximized ? '0' : '1'}px`;
|
`${maximized ? '0' : '1'}px`;
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
<div
|
<div
|
||||||
style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
|
style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
|
||||||
className={css('main', isMac && 'mainRounded')}
|
className={css('main', isMac_ && 'mainRounded')}
|
||||||
>
|
>
|
||||||
<HeaderContainer/>
|
<HeaderContainer/>
|
||||||
<TermsContainer ref_={this.onTermsRef}/>
|
<TermsContainer ref_={this.onTermsRef}/>
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ const TermsContainer = connect(
|
||||||
},
|
},
|
||||||
|
|
||||||
onTitle(uid, title) {
|
onTitle(uid, title) {
|
||||||
// we need to trim the title because `cmd.exe` likes to report ' ' as the title
|
console.log(title)
|
||||||
dispatch(setSessionXtermTitle(uid, title.trim()));
|
dispatch(setSessionXtermTitle(uid, title));
|
||||||
},
|
},
|
||||||
|
|
||||||
onResize(uid, cols, rows) {
|
onResize(uid, cols, rows) {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ const reducer = (state = initialState, action) => {
|
||||||
if (state.sessions[action.uid]) {
|
if (state.sessions[action.uid]) {
|
||||||
return deleteSession(state, action.uid);
|
return deleteSession(state, action.uid);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log('ignore pty exit: session removed by user');
|
console.log('ignore pty exit: session removed by user');
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
|
|
@ -90,7 +91,12 @@ const reducer = (state = initialState, action) => {
|
||||||
return deleteSession(state, action.uid);
|
return deleteSession(state, action.uid);
|
||||||
|
|
||||||
case SESSION_SET_XTERM_TITLE:
|
case SESSION_SET_XTERM_TITLE:
|
||||||
return state.setIn(['sessions', action.uid, 'title'], action.title);
|
return state.setIn(
|
||||||
|
['sessions', action.uid, 'title'],
|
||||||
|
// we need to trim the title because `cmd.exe`
|
||||||
|
// likes to report ' ' as the title
|
||||||
|
action.title.trim()
|
||||||
|
);
|
||||||
|
|
||||||
case SESSION_RESIZE:
|
case SESSION_RESIZE:
|
||||||
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({
|
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue