From 27a20e0cfc7cb5dbafbad1b7a0600393f5d7553d Mon Sep 17 00:00:00 2001 From: Matheus Fernandes Date: Wed, 16 Nov 2016 16:44:04 -0200 Subject: [PATCH] Add support for composed chars and fix issues with "foreign" keyboard layouts (#1006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- app/index.js | 3 +- lib/components/term.js | 48 ++++++++++-- lib/hterm.js | 163 +++++++++++++++++++++++++---------------- 3 files changed, 142 insertions(+), 72 deletions(-) diff --git a/app/index.js b/app/index.js index ae761ea6..bfa7dbdd 100644 --- a/app/index.js +++ b/app/index.js @@ -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); diff --git a/lib/components/term.js b/lib/components/term.js index 9ab873b4..43b7f3f2 100644 --- a/lib/components/term.js +++ b/lib/components/term.js @@ -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%' }} /> : -
+ [ +
, +
+ ] } { this.props.customChildren }
); diff --git a/lib/hterm.js b/lib/hterm.js index ff2deb16..d56febdf 100644 --- a/lib/hterm.js +++ b/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 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) {