mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
Add support for composed chars and fix issues with "foreign" keyboard layouts (#1006)
* ime * Fix code style * Add visual feedback for composition events * Temporarily disable `hterm`'s `onKeyDown` hacks * Replicate the focus/blur state of our caret on the `hterm` caret * Fix: focus our caret when there's a tab change * `caret_` => `hyperCaret` * Reorg: move the caret hacks to the `hterm.js` extensions * Remove `console.log` * Remove the `Dead key` hack and reenable keyboard commands * Add a (temporary?) fix to re-enable text selection * Check for a selection `onMouseUp` instead `onFocus` * Fix wrong buggy hterm's cursor styling on term focus/blur * Fix the cursor style after the bell rings – closes #674 * Enable `acceptFirstMouse` to focus the correct term – closes #861 * Fix code style * Fix: clear the `hyper-caret` when a char is inserted via the IME dialog * Remove useless function * Add coments regarding text selection * Fix code style
This commit is contained in:
parent
0585607da4
commit
27a20e0cfc
3 changed files with 142 additions and 72 deletions
|
|
@ -121,7 +121,8 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
|||
// HYPERTERM_DEBUG for backwards compatibility with hyperterm
|
||||
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
|
||||
x: startX,
|
||||
y: startY
|
||||
y: startY,
|
||||
acceptFirstMouse: true
|
||||
};
|
||||
const browserOptions = plugins.getDecoratedBrowserOptions(browserDefaults);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,15 +13,18 @@ export default class Term extends Component {
|
|||
super(props);
|
||||
this.handleWheel = this.handleWheel.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleScrollEnter = this.handleScrollEnter.bind(this);
|
||||
this.handleScrollLeave = this.handleScrollLeave.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.onHyperCaret = this.onHyperCaret.bind(this);
|
||||
props.ref_(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {props} = this;
|
||||
this.term = props.term || new hterm.Terminal(uuid.v4());
|
||||
this.term.onHyperCaret(this.hyperCaret);
|
||||
|
||||
// the first term that's created has unknown size
|
||||
// subsequent new tabs have size
|
||||
|
|
@ -77,6 +80,7 @@ export default class Term extends Component {
|
|||
// emit onTitle event when hterm instance
|
||||
// wants to set the title of its tab
|
||||
this.term.setWindowTitle = props.onTitle;
|
||||
this.term.focusHyperCaret();
|
||||
};
|
||||
this.term.decorate(this.termRef);
|
||||
this.term.installKeyboard();
|
||||
|
|
@ -87,7 +91,7 @@ export default class Term extends Component {
|
|||
const iframeWindow = this.getTermDocument().defaultView;
|
||||
iframeWindow.addEventListener('wheel', this.handleWheel);
|
||||
|
||||
this.getScreenNode().addEventListener('focus', this.handleFocus);
|
||||
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleWheel(e) {
|
||||
|
|
@ -122,6 +126,22 @@ export default class Term extends Component {
|
|||
// called, which is unecessary.
|
||||
// Should investigate if it matters.
|
||||
this.props.onActive();
|
||||
this.term.focusHyperCaret();
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
this.props.onActive();
|
||||
// this makes sure that we focus the hyper caret only
|
||||
// if a click on the term does not result in a selection
|
||||
// otherwise, if we focus without such check, it'd be
|
||||
// impossible to select a piece of text
|
||||
if (this.term.document_.getSelection().type !== 'Range') {
|
||||
this.term.focusHyperCaret();
|
||||
}
|
||||
}
|
||||
|
||||
onHyperCaret(caret) {
|
||||
this.hyperCaret = caret;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
|
|
@ -129,7 +149,7 @@ export default class Term extends Component {
|
|||
}
|
||||
|
||||
focus() {
|
||||
this.term.focus();
|
||||
this.term.focusHyperCaret();
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
|
@ -185,6 +205,14 @@ export default class Term extends Component {
|
|||
|
||||
getStylesheet(css) {
|
||||
const blob = new Blob([`
|
||||
.hyper-caret {
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 black;
|
||||
font-family: ${this.props.fontFamily};
|
||||
font-size: ${this.props.fontSize}px;
|
||||
}
|
||||
.cursor-node[focus="false"] {
|
||||
border-width: 1px !important;
|
||||
}
|
||||
|
|
@ -237,6 +265,7 @@ export default class Term extends Component {
|
|||
|
||||
if (this.props.fontSize !== nextProps.fontSize) {
|
||||
prefs.set('font-size', nextProps.fontSize);
|
||||
this.hyperCaret.style.fontSize = nextProps.fontSize + 'px';
|
||||
}
|
||||
|
||||
if (this.props.foregroundColor !== nextProps.foregroundColor) {
|
||||
|
|
@ -245,6 +274,7 @@ export default class Term extends Component {
|
|||
|
||||
if (this.props.fontFamily !== nextProps.fontFamily) {
|
||||
prefs.set('font-family', nextProps.fontFamily);
|
||||
this.hyperCaret.style.fontFamily = nextProps.fontFamily;
|
||||
}
|
||||
|
||||
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
|
||||
|
|
@ -315,11 +345,15 @@ export default class Term extends Component {
|
|||
height: '100%'
|
||||
}}
|
||||
/> :
|
||||
<div
|
||||
className={css('scrollbarShim')}
|
||||
onMouseEnter={this.handleScrollEnter}
|
||||
onMouseLeave={this.handleScrollLeave}
|
||||
/>
|
||||
[
|
||||
<div key="hyper-caret" contentEditable className="hyper-caret" ref={this.onHyperCaret}/>,
|
||||
<div // eslint-disable-line react/jsx-indent
|
||||
key="scrollbar"
|
||||
className={css('scrollbarShim')}
|
||||
onMouseEnter={this.handleScrollEnter}
|
||||
onMouseLeave={this.handleScrollLeave}
|
||||
/>
|
||||
]
|
||||
}
|
||||
{ this.props.customChildren }
|
||||
</div>);
|
||||
|
|
|
|||
163
lib/hterm.js
163
lib/hterm.js
|
|
@ -42,69 +42,6 @@ const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
|
|||
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
|
||||
const modifierKeysConf = this.terminal.modifierKeys;
|
||||
|
||||
/**
|
||||
* Add fixes for U.S. International PC Keyboard layout
|
||||
* These keys are sent through as 'Dead' keys, as they're used as modifiers.
|
||||
* Ignore that and insert the correct character.
|
||||
*/
|
||||
if (e.key === 'Dead') {
|
||||
if (e.code === 'Quote' && e.shiftKey === false) {
|
||||
this.terminal.onVTKeystroke('\'');
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Quote' && e.shiftKey === true) {
|
||||
this.terminal.onVTKeystroke('"');
|
||||
return;
|
||||
}
|
||||
if ((e.code === 'IntlBackslash' || e.code === 'Backquote') && e.shiftKey === true) {
|
||||
this.terminal.onVTKeystroke('~');
|
||||
return;
|
||||
}
|
||||
// This key is also a tilde on all tested keyboards
|
||||
if (e.code === 'KeyN' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
|
||||
this.terminal.onVTKeystroke('~');
|
||||
return;
|
||||
}
|
||||
if ((e.code === 'IntlBackslash' || e.code === 'Backquote') && e.shiftKey === false) {
|
||||
this.terminal.onVTKeystroke('`');
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Digit6') {
|
||||
this.terminal.onVTKeystroke('^');
|
||||
return;
|
||||
}
|
||||
// German keyboard layout
|
||||
if (e.code === 'Equal' && e.shiftKey === false) {
|
||||
this.terminal.onVTKeystroke('´');
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Equal' && e.shiftKey === true) {
|
||||
this.terminal.onVTKeystroke('`');
|
||||
return;
|
||||
}
|
||||
// Italian keyboard layout
|
||||
if (e.code === 'Digit9' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
|
||||
this.terminal.onVTKeystroke('`');
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Digit8' && e.altKey === true && modifierKeysConf.altIsMeta === false) {
|
||||
this.terminal.onVTKeystroke('´');
|
||||
// To fix issue with changing the terminal prompt
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// French keyboard layout
|
||||
if (e.code === 'BracketLeft') {
|
||||
this.terminal.onVTKeystroke('^');
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Backslash') {
|
||||
this.terminal.onVTKeystroke('`');
|
||||
return;
|
||||
}
|
||||
console.warn('Uncaught dead key on international keyboard', e);
|
||||
}
|
||||
|
||||
if (e.altKey &&
|
||||
e.which !== 16 && // Ignore other modifer keys
|
||||
e.which !== 17 &&
|
||||
|
|
@ -132,7 +69,9 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
|
|||
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) {
|
||||
return;
|
||||
}
|
||||
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
|
||||
if ((!e.ctrlKey || e.code !== 'ControlLeft') &&
|
||||
!e.shiftKey && e.code !== 'CapsLock' &&
|
||||
e.key !== 'Dead') {
|
||||
// Test for valid keys in order to clear the terminal selection
|
||||
selection.clear(this.terminal);
|
||||
}
|
||||
|
|
@ -212,6 +151,102 @@ hterm.ScrollPort.prototype.setBackgroundColor = function () {
|
|||
this.screen_.style.backgroundColor = 'transparent';
|
||||
};
|
||||
|
||||
// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
|
||||
hterm.Terminal.prototype.onHyperCaret = function (caret) {
|
||||
this.hyperCaret = caret;
|
||||
let ongoingComposition = false;
|
||||
|
||||
caret.addEventListener('compositionstart', () => {
|
||||
ongoingComposition = true;
|
||||
});
|
||||
|
||||
// we can ignore `compositionstart` since chromium always fire it with ''
|
||||
caret.addEventListener('compositionupdate', () => {
|
||||
this.cursorNode_.style.backgroundColor = 'yellow';
|
||||
this.cursorNode_.style.borderColor = 'yellow';
|
||||
});
|
||||
|
||||
// at this point the char(s) is ready
|
||||
caret.addEventListener('compositionend', () => {
|
||||
ongoingComposition = false;
|
||||
this.cursorNode_.style.backgroundColor = '';
|
||||
this.setCursorShape(this.getCursorShape());
|
||||
this.cursorNode_.style.borderColor = this.getCursorColor();
|
||||
caret.innerText = '';
|
||||
});
|
||||
|
||||
// if you open the `Emoji & Symbols` (ctrl+cmd+space)
|
||||
// and select an emoji, it'll be inserted into our caret
|
||||
// and stay there until you star a compositon event.
|
||||
// to avoid that, we'll just check if there's an ongoing
|
||||
// compostion event. if there's one, we do nothing.
|
||||
// otherwise, we just remove the emoji and stop the event
|
||||
// propagation.
|
||||
// PS: this event will *not* be fired when a standard char
|
||||
// (a, b, c, 1, 2, 3, etc) is typed – only for composed
|
||||
// ones and `Emoji & Symbols`
|
||||
caret.addEventListener('input', e => {
|
||||
if (!ongoingComposition) {
|
||||
caret.innerText = '';
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// we need to capture pastes, prevent them and send its contents to the terminal
|
||||
caret.addEventListener('paste', e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text');
|
||||
this.onVTKeystroke(text);
|
||||
});
|
||||
|
||||
// here we replicate the focus/blur state of our caret on the `hterm` caret
|
||||
caret.addEventListener('focus', () => {
|
||||
this.cursorNode_.setAttribute('focus', true);
|
||||
this.restyleCursor_();
|
||||
});
|
||||
caret.addEventListener('blur', () => {
|
||||
this.cursorNode_.setAttribute('focus', false);
|
||||
this.restyleCursor_();
|
||||
});
|
||||
|
||||
// this is necessary because we need to access the `document_` and the hyperCaret
|
||||
// on `hterm.Screen.prototype.syncSelectionCaret`
|
||||
this.primaryScreen_.terminal = this;
|
||||
this.alternateScreen_.terminal = this;
|
||||
};
|
||||
|
||||
// ensure that our contenteditable caret is injected
|
||||
// inside the term's cursor node and that it's focused
|
||||
hterm.Terminal.prototype.focusHyperCaret = function () {
|
||||
if (!this.hyperCaret.parentNode !== this.cursorNode_) {
|
||||
this.cursorNode_.appendChild(this.hyperCaret);
|
||||
}
|
||||
this.hyperCaret.focus();
|
||||
};
|
||||
|
||||
hterm.Screen.prototype.syncSelectionCaret = function () {
|
||||
const p = this.terminal.hyperCaret;
|
||||
const doc = this.terminal.document_;
|
||||
const win = doc.defaultView;
|
||||
const s = win.getSelection();
|
||||
const r = doc.createRange();
|
||||
r.selectNodeContents(p);
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
};
|
||||
|
||||
// fixes a bug in hterm, where the cursor goes back to `BLOCK`
|
||||
// after the bell rings
|
||||
const oldRingBell = hterm.Terminal.prototype.ringBell;
|
||||
hterm.Terminal.prototype.ringBell = function () {
|
||||
oldRingBell.call(this);
|
||||
setTimeout(() => {
|
||||
this.restyleCursor_();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
// fixes a bug in hterm, where the shorthand hex
|
||||
// is not properly converted to rgb
|
||||
lib.colors.hexToRGB = function (arg) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue