From b302962d444309c5e8c66ce2ebd51ba17ecb2f8c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 2 Aug 2017 12:05:47 -0700 Subject: [PATCH] latest --- lib/components/term-group.js | 33 ++++++--- lib/components/term.js | 135 +++++++++++++++++++++++++++++------ lib/components/terms.js | 3 +- lib/containers/hyper.js | 46 ++---------- lib/containers/terms.js | 4 +- lib/reducers/sessions.js | 8 ++- 6 files changed, 156 insertions(+), 73 deletions(-) diff --git a/lib/components/term-group.js b/lib/components/term-group.js index 7414326d..f53aaf08 100644 --- a/lib/components/term-group.js +++ b/lib/components/term-group.js @@ -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 ( 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) { diff --git a/lib/components/term.js b/lib/components/term.js index 32e3e13e..d718afc3 100644 --- a/lib/components/term.js +++ b/lib/components/term.js @@ -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 (
{ - this.termWrapperRef = component; - }} className={css('fit', this.props.isTermActive && 'active')} style={{padding: this.props.padding}} > { this.props.customChildrenBefore }
{ - this.termRef = component; + this.termWrapperRef = component; }} - className={css('fit', 'term')} - /> + className={css('fit', 'wrapper')} + > +
{ + this.termRef = component; + }} + className={css('term')} + /> +
{ this.props.customChildren }
); } @@ -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: {} }; } diff --git a/lib/components/terms.js b/lib/components/terms.js index 041a4e77..f05cb88e 100644 --- a/lib/components/terms.js +++ b/lib/components/terms.js @@ -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, diff --git a/lib/containers/hyper.js b/lib/containers/hyper.js index 5a110e50..ae157615 100644 --- a/lib/containers/hyper.js +++ b/lib/containers/hyper.js @@ -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 (
diff --git a/lib/containers/terms.js b/lib/containers/terms.js index dfce7438..012012d8 100644 --- a/lib/containers/terms.js +++ b/lib/containers/terms.js @@ -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) { diff --git a/lib/reducers/sessions.js b/lib/reducers/sessions.js index 85043f91..18bffff4 100644 --- a/lib/reducers/sessions.js +++ b/lib/reducers/sessions.js @@ -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({