hyper/lib/hterm.js
Martin Ek a7595c1a45 Split Panes (#693)
* npm: add .npmrc with save-exact=true

* split panes: create initial implementation

This allows users to split their Hyperterm terms into
multiple nested splits, both vertical and horizontal.

Fixes #56

* split panes: suport closing tabs and individual panes

* split panes: ensure new splits are placed at the correct index

New split panes should be placed after the currently active
pane, not at the end like they were previously.

* split panes: add explicit dependency to uuid

* split panes: implement split pane cycling

This adds menu buttons for moving back and forward between
open split panes in the currect terminal tab.
Doesn't add a hotkey yet, needs some bikeshedding.

* split panes: move activeSessionUid to its own object

It made little sense to have so many objects with `activeSessionUid`
set to `null` when it only mattered on the top level.
Now it's an object mapping term-group `uid` to `sessionUid` instead.

* split panes: make sure closing the last split pane exits the app

* split panes: fix a crash after closing specific panes

Sometimes the terminal would crash when a specific
split pane was closed, because the `activeSessions`
mapping wasn't updated correctly.

* split panes: fix a bug that caused initial session sizing to be wrong

* fix all our focus / blur issues in one fell swoop :O (famous last words)

* get rid of react warning

* hterm: make sure not to lose focus when VT listens on clicks

* term: restore onactive callback

* add missing `return` to override (just in case)

* split pane: new split pane implementation

* goodbye react-split-pane

* added term group resizing action and reducer

* terms: supply border color so that we can use it for splits

* term-group: add resizing hook

* term-groups: add resizing constant

* remove split pane css side-effect

* split panes: pass existing hterm instances to Term

* split panes: add keybindings for split pane cycling

* split panes: remove unused action

* split panes: remove unused styling

* split-pane: remove `console.log`

* split-pane: remove `console.log`

* split panes: rebalance sizes on insert/removal

* split panes: pass existing hterm instances to Term

* split panes: add keybindings for split pane cycling

* split panes: remove unused action

* split panes: remove unused styling

* split panes: rebalance sizes on insert/removal

* split panes: set a minimum size for resizing

* split-pane: fix vertical splits

* css :|

* package: bump electron

* split panes: attach onFocus listener to webviews

* 1.4.1 and 1.4.2 are broken. they have the following regression:
- open google.com on the main window
- open a new tab
- come back to previous tab. webview is gone :|

* split panes: handle PTY exits

* split panes: add linux friendly keybindings
2016-10-03 19:00:50 -07:00

274 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {hterm, lib} from 'hterm-umdjs';
import fromCharCode from './utils/key-code';
const selection = require('./utils/selection');
// Keys that are used in combination
// with ctrl based hotkeys:
const IGNORED_CTRL_KEYS = [
'Tab',
'KeyO',
'KeyE'
];
hterm.defaultStorage = new lib.Storage.Memory();
// Provide selectAll to terminal viewport
hterm.Terminal.prototype.selectAll = function () {
// We need to clear 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('[hyperterm+hterm] ignore double click');
return;
}
return oldMouse.call(this, e);
};
// 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 !== null && text !== '') {
this.copyStringToClipboard(text);
}
};
// passthrough all the commands that are meant to control
// hyperterm and not the terminal itself
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 &&
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.metaKey || e.altKey || (e.ctrlKey && IGNORED_CTRL_KEYS.includes(e.code))) {
return;
}
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
// Test for valid keys in order to clear the terminal selection
selection.clear(this.terminal);
}
return oldKeyDown.call(this, e);
};
const oldKeyPress = hterm.Keyboard.prototype.onKeyPress_;
hterm.Keyboard.prototype.onKeyPress_ = function (e) {
if (e.metaKey) {
return;
}
selection.clear(this.terminal);
return oldKeyPress.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);
};
// sine above we're no longer relying on `preventDefault`
// to avoid selections, we use css instead, so that
// focus is not lost, but selections are still not possible
// when the appropiate VT mode is set
hterm.VT.prototype.__defineSetter__('mouseReport', function (val) {
this.mouseReport_ = val;
const rowNodes = this.terminal.scrollPort_.rowNodes_;
if (rowNodes) {
if (val === this.MOUSE_REPORT_DISABLED) {
rowNodes.style.webkitUserSelect = 'text';
} else {
rowNodes.style.webkitUserSelect = 'none';
}
}
});
hterm.VT.prototype.__defineGetter__('mouseReport', function () {
return this.mouseReport_;
});
// 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};