mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 04:28:41 -09:00
431 lines
12 KiB
JavaScript
431 lines
12 KiB
JavaScript
import {hterm, lib} from 'hterm-umdjs';
|
||
import runes from 'runes';
|
||
|
||
import {isAccelerator} from '../app/accelerators';
|
||
|
||
import fromCharCode from './utils/key-code';
|
||
import selection from './utils/selection';
|
||
|
||
hterm.defaultStorage = new lib.Storage.Memory();
|
||
|
||
// Provide selectAll to terminal viewport
|
||
hterm.Terminal.prototype.selectAll = function () {
|
||
// If the cursorNode_ having hyperCaret we need to remove it
|
||
if (this.cursorNode_.contains(this.hyperCaret)) {
|
||
this.cursorNode_.removeChild(this.hyperCaret);
|
||
// We need to clear the DOM range to reset anchorNode
|
||
selection.clear(this);
|
||
selection.all(this);
|
||
}
|
||
};
|
||
|
||
// override double click behavior to copy
|
||
const oldMouse = hterm.Terminal.prototype.onMouse_;
|
||
hterm.Terminal.prototype.onMouse_ = function (e) {
|
||
if (e.type === 'dblclick') {
|
||
selection.extend(this);
|
||
console.log('[hyper+hterm] ignore double click');
|
||
return;
|
||
}
|
||
return oldMouse.call(this, e);
|
||
};
|
||
|
||
function containsNonLatinCodepoints(s) {
|
||
return /[^\u0000-\u00ff]/.test(s);
|
||
}
|
||
|
||
// hterm Unicode patch
|
||
hterm.TextAttributes.splitWidecharString = function (str) {
|
||
const context = runes(str).reduce((ctx, rune) => {
|
||
const code = rune.codePointAt(0);
|
||
if (code < 128 || lib.wc.charWidth(code) === 1) {
|
||
ctx.acc += rune;
|
||
return ctx;
|
||
}
|
||
if (ctx.acc) {
|
||
ctx.items.push({str: ctx.acc});
|
||
ctx.acc = '';
|
||
}
|
||
ctx.items.push({str: rune, wcNode: true});
|
||
return ctx;
|
||
}, {items: [], acc: ''});
|
||
if (context.acc) {
|
||
context.items.push({str: context.acc});
|
||
}
|
||
return context.items;
|
||
};
|
||
|
||
// hterm Unicode patch
|
||
lib.wc.strWidth = function (str) {
|
||
const chars = runes(str);
|
||
let width = 0;
|
||
let rv = 0;
|
||
|
||
for (let i = 0; i < chars.length; i++) {
|
||
const codePoint = chars[i].codePointAt(0);
|
||
width = lib.wc.charWidth(codePoint);
|
||
if (width < 0) {
|
||
return -1;
|
||
}
|
||
rv += width * ((codePoint <= 0xffff) ? 1 : 2);
|
||
}
|
||
return rv;
|
||
};
|
||
|
||
// hterm Unicode patch
|
||
lib.wc.substr = function (str, start, optWidth) {
|
||
const chars = runes(str);
|
||
let startIndex;
|
||
let endIndex;
|
||
let width = 0;
|
||
|
||
for (let i = 0; i < chars.length; i++) {
|
||
const codePoint = chars[i].codePointAt(0);
|
||
const charWidth = lib.wc.charWidth(codePoint);
|
||
if ((width + charWidth) > start) {
|
||
startIndex = i;
|
||
break;
|
||
}
|
||
width += charWidth;
|
||
}
|
||
|
||
if (optWidth) {
|
||
width = 0;
|
||
for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) {
|
||
width += lib.wc.charWidth(chars[endIndex].charCodeAt(0));
|
||
}
|
||
|
||
if (width > optWidth) {
|
||
endIndex--;
|
||
}
|
||
return chars.slice(startIndex, endIndex).join('');
|
||
}
|
||
return chars.slice(startIndex).join('');
|
||
};
|
||
|
||
// MacOS emoji bar support
|
||
hterm.Keyboard.prototype.onTextInput_ = function (e) {
|
||
if (!e.data) {
|
||
return;
|
||
}
|
||
runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal));
|
||
};
|
||
|
||
hterm.Terminal.IO.prototype.writeUTF8 = function (string) {
|
||
if (this.terminal_.io !== this) {
|
||
throw new Error('Attempt to print from inactive IO object.');
|
||
}
|
||
|
||
if (!containsNonLatinCodepoints(string)) {
|
||
this.terminal_.interpret(string);
|
||
return;
|
||
}
|
||
|
||
runes(string).forEach(rune => {
|
||
this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune);
|
||
this.terminal_.interpret(rune);
|
||
this.terminal_.getTextAttributes().unicodeNode = false;
|
||
});
|
||
};
|
||
|
||
const oldIsDefault = hterm.TextAttributes.prototype.isDefault;
|
||
hterm.TextAttributes.prototype.isDefault = function () {
|
||
return !this.unicodeNode && oldIsDefault.call(this);
|
||
};
|
||
|
||
const oldSetFontSize = hterm.Terminal.prototype.setFontSize;
|
||
hterm.Terminal.prototype.setFontSize = function (px) {
|
||
oldSetFontSize.call(this, px);
|
||
const doc = this.getDocument();
|
||
let unicodeNodeStyle = doc.getElementById('hyper-unicode-styles');
|
||
if (!unicodeNodeStyle) {
|
||
unicodeNodeStyle = doc.createElement('style');
|
||
unicodeNodeStyle.setAttribute('id', 'hyper-unicode-styles');
|
||
doc.head.appendChild(unicodeNodeStyle);
|
||
}
|
||
unicodeNodeStyle.innerHTML = `
|
||
.unicode-node {
|
||
display: inline-block;
|
||
width: ${this.scrollPort_.characterSize.width}px;
|
||
}
|
||
`;
|
||
};
|
||
|
||
const oldCreateContainer = hterm.TextAttributes.prototype.createContainer;
|
||
hterm.TextAttributes.prototype.createContainer = function (text) {
|
||
const container = oldCreateContainer.call(this, text);
|
||
if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) {
|
||
container.className += ' unicode-node';
|
||
}
|
||
return container;
|
||
};
|
||
|
||
// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers)
|
||
const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer;
|
||
hterm.TextAttributes.prototype.matchesContainer = function (obj) {
|
||
return oldMatchesContainer.call(this, obj) &&
|
||
!this.unicodeNode &&
|
||
!containsNonLatinCodepoints(obj.textContent);
|
||
};
|
||
|
||
// there's no option to turn off the size overlay
|
||
hterm.Terminal.prototype.overlaySize = function () {};
|
||
|
||
// fixing a bug in hterm where a double click triggers
|
||
// a non-collapsed selection whose text is '', and results
|
||
// in an infinite copy loop
|
||
hterm.Terminal.prototype.copySelectionToClipboard = function () {
|
||
const text = this.getSelectionText();
|
||
if (text) {
|
||
this.copyStringToClipboard(text);
|
||
}
|
||
};
|
||
|
||
// passthrough all the commands that are meant to control
|
||
// hyper and not the terminal itself
|
||
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
|
||
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
|
||
const modifierKeysConf = this.terminal.modifierKeys;
|
||
|
||
if (e.altKey &&
|
||
e.which !== 16 && // Ignore other modifer keys
|
||
e.which !== 17 &&
|
||
e.which !== 18 &&
|
||
e.which !== 91 &&
|
||
modifierKeysConf.altIsMeta) {
|
||
const char = fromCharCode(e);
|
||
this.terminal.onVTKeystroke('\x1b' + char);
|
||
e.preventDefault();
|
||
}
|
||
|
||
if (e.metaKey &&
|
||
e.code !== 'MetaLeft' &&
|
||
e.code !== 'MetaRight' &&
|
||
e.which !== 16 &&
|
||
e.which !== 17 &&
|
||
e.which !== 18 &&
|
||
e.which !== 91 &&
|
||
modifierKeysConf.cmdIsMeta) {
|
||
const char = fromCharCode(e);
|
||
this.terminal.onVTKeystroke('\x1b' + char);
|
||
e.preventDefault();
|
||
}
|
||
|
||
if (e.altKey || e.metaKey || isAccelerator(e)) {
|
||
// hterm shouldn't consume a hyper accelerator
|
||
// // Was the hyperCaret removed for selectAll
|
||
if (e.key === 'v' && !this.terminal.cursorNode_.contains(this.hyperCaret)) {
|
||
this.terminal.focusHyperCaret();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Test for valid keys in order to accept clear status
|
||
if (e.code !== 'Control' && e.key !== 'Shift' && e.code !== 'CapsLock' && e.key !== 'Dead') {
|
||
selection.clear(this.terminal);
|
||
}
|
||
|
||
// Was the hyperCaret removed for selectAll
|
||
if (!this.terminal.cursorNode_.contains(this.hyperCaret)) {
|
||
this.terminal.focusHyperCaret();
|
||
}
|
||
return oldKeyDown.call(this, e);
|
||
};
|
||
|
||
// we re-implement `wipeContents` to preserve the line
|
||
// and cursor position that the client is in.
|
||
// otherwise the user ends up with a completely clear
|
||
// screen which is really strange
|
||
hterm.Terminal.prototype.clearPreserveCursorRow = function () {
|
||
this.scrollbackRows_.length = 0;
|
||
this.scrollPort_.resetCache();
|
||
|
||
[this.primaryScreen_, this.alternateScreen_].forEach(screen => {
|
||
const bottom = screen.getHeight();
|
||
if (bottom > 0) {
|
||
this.renumberRows_(0, bottom);
|
||
|
||
const x = screen.cursorPosition.column;
|
||
const y = screen.cursorPosition.row;
|
||
|
||
if (x === 0) {
|
||
// Empty screen, nothing to do.
|
||
return;
|
||
}
|
||
|
||
// here we move the row that the user was focused on
|
||
// to the top of the screen
|
||
this.moveRows_(y, 1, 0);
|
||
|
||
for (let i = 1; i < bottom; i++) {
|
||
screen.setCursorPosition(i, 0);
|
||
screen.clearCursorRow();
|
||
}
|
||
|
||
// we restore the cursor position
|
||
screen.setCursorPosition(0, x);
|
||
}
|
||
});
|
||
|
||
this.syncCursorPosition_();
|
||
this.scrollPort_.invalidate();
|
||
|
||
// this will avoid a bug where the `wipeContents`
|
||
// hterm API doesn't send the scroll to the top
|
||
this.scrollPort_.redraw_();
|
||
};
|
||
|
||
const oldOnMouse = hterm.Terminal.prototype.onMouse_;
|
||
hterm.Terminal.prototype.onMouse_ = function (e) {
|
||
// override `preventDefault` to not actually
|
||
// prevent default when the type of event is
|
||
// mousedown, so that we can still trigger
|
||
// focus on the terminal when the underlying
|
||
// VT is interested in mouse events, as is the
|
||
// case of programs like `vtop` that allow for
|
||
// the user to click on rows
|
||
if (e.type === 'mousedown') {
|
||
e.preventDefault = function () { };
|
||
}
|
||
|
||
return oldOnMouse.call(this, e);
|
||
};
|
||
|
||
// make background transparent to avoid transparency issues
|
||
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) {
|
||
const hex16 = lib.colors.re_.hex16;
|
||
const hex24 = lib.colors.re_.hex24;
|
||
|
||
function convert(hex) {
|
||
if (hex.length === 4) {
|
||
hex = hex.replace(hex16, (h, r, g, b) => {
|
||
return '#' + r + r + g + g + b + b;
|
||
});
|
||
}
|
||
const ary = hex.match(hex24);
|
||
if (!ary) {
|
||
return null;
|
||
}
|
||
|
||
return 'rgb(' +
|
||
parseInt(ary[1], 16) + ', ' +
|
||
parseInt(ary[2], 16) + ', ' +
|
||
parseInt(ary[3], 16) +
|
||
')';
|
||
}
|
||
|
||
if (arg instanceof Array) {
|
||
for (let i = 0; i < arg.length; i++) {
|
||
arg[i] = convert(arg[i]);
|
||
}
|
||
} else {
|
||
arg = convert(arg);
|
||
}
|
||
|
||
return arg;
|
||
};
|
||
|
||
export default hterm;
|
||
export {lib};
|