mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18: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) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue