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) {