This commit is contained in:
Guillermo Rauch 2017-08-02 12:05:47 -07:00
parent 6a1dcf9ef0
commit b302962d44
6 changed files with 156 additions and 73 deletions

View file

@ -14,6 +14,7 @@ class TermGroup_ extends Component {
constructor(props, context) {
super(props, context);
this.bound = new WeakMap();
this.termRefs = {}
}
bind(fn, thisObj, uid) {
@ -50,19 +51,13 @@ class TermGroup_ extends Component {
const props = getTermProps(uid, this.props, {
isTermActive: uid === this.props.activeSession,
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,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
modifierKeys: this.props.modifierKeys,
padding: this.props.padding,
colors: this.props.colors,
url: session.url,
cleared: session.cleared,
cols: session.cols,
@ -84,12 +79,34 @@ class TermGroup_ extends Component {
// which is inefficient. Should maybe do something similar
// to this.bind.
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}
{...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() {
const {childGroups, termGroup} = this.props;
if (termGroup.sessionUid) {

View file

@ -17,20 +17,32 @@ export default class Term extends Component {
super(props);
props.ref_(this);
this.termRef = null
this.termWrapperRef = null
this.termRect = null
this.onOpen = this.onOpen.bind(this)
this.onWindowResize = this.onWindowResize.bind(this)
}
componentDidMount() {
const {props} = this;
this.term = props.term || new Terminal({
cursorStyle: CURSOR_STYLES[props.cursorShape],
cursorBlink: props.cursorBlink,
cols: props.cols,
rows: props.rows
});
this.term.open(this.termRef)
// we need to use this hack to retain the term reference
// as we move the term around splits, until xterm adds
// support for getState / setState
if (props.term) {
this.term = props.term
this.termRef.appendChild(this.term.element)
this.onOpen()
} 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) {
this.term.on(
@ -42,7 +54,7 @@ export default class Term extends Component {
if (props.onActive) {
this.term.on(
'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;
}
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 () {
// eslint-disable-next-line no-console
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() {
// eslint-disable-next-line no-console
console.error('unimplemented')
this.measureResize();
}
write(data) {
@ -87,35 +129,84 @@ export default class Term extends Component {
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) {
if (!this.props.cleared && nextProps.cleared) {
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() {
terms[this.props.uid] = this;
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) {
return (<div
ref={component => {
this.termWrapperRef = component;
}}
className={css('fit', this.props.isTermActive && 'active')}
style={{padding: this.props.padding}}
>
{ this.props.customChildrenBefore }
<div
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 }
</div>);
}
@ -127,7 +218,11 @@ export default class Term extends Component {
width: '100%',
height: '100%'
},
wrapper: {
// TODO: decide whether to keep this or not based on
// understanding what xterm-selection is for
overflow: 'hidden'
},
term: {}
};
}

View file

@ -87,9 +87,10 @@ export default class Terms extends Component {
borderColor: this.props.borderColor,
cursorShape: this.props.cursorShape,
cursorBlink: this.props.cursorBlink,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
padding: this.props.padding,
colors: this.props.colors,
bell: this.props.bell,
bellSoundURL: this.props.bellSoundURL,
copyOnSelect: this.props.copyOnSelect,

View file

@ -1,6 +1,5 @@
/* eslint-disable react/no-danger */
import Mousetrap from 'mousetrap';
import React from 'react';
import Component from '../component';
@ -36,43 +35,8 @@ class Hyper extends Component {
}
attachKeyListeners() {
const {moveTo, moveLeft, moveRight} = this.props;
const term = this.terms.getActiveTerm();
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;
// eslint-disable-next-line no-console
console.error('removed key listeners')
}
onTermsRef(terms) {
@ -97,14 +61,14 @@ class Hyper extends Component {
}
template(css) {
const {isMac, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac ? '' :
const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac_ ? '' :
`${maximized ? '0' : '1'}px`;
return (<div>
<div
style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
className={css('main', isMac && 'mainRounded')}
className={css('main', isMac_ && 'mainRounded')}
>
<HeaderContainer/>
<TermsContainer ref_={this.onTermsRef}/>

View file

@ -49,8 +49,8 @@ const TermsContainer = connect(
},
onTitle(uid, title) {
// we need to trim the title because `cmd.exe` likes to report ' ' as the title
dispatch(setSessionXtermTitle(uid, title.trim()));
console.log(title)
dispatch(setSessionXtermTitle(uid, title));
},
onResize(uid, cols, rows) {

View file

@ -83,6 +83,7 @@ const reducer = (state = initialState, action) => {
if (state.sessions[action.uid]) {
return deleteSession(state, action.uid);
}
// eslint-disable-next-line no-console
console.log('ignore pty exit: session removed by user');
return state;
@ -90,7 +91,12 @@ const reducer = (state = initialState, action) => {
return deleteSession(state, action.uid);
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:
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({