Fixed merge conflicts

This commit is contained in:
Leo Lamprecht 2018-04-16 07:17:17 -07:00
parent 4d7ce43755
commit 4ce99863c4
133 changed files with 74506 additions and 53064 deletions

97
.circleci/config.yml Normal file
View file

@ -0,0 +1,97 @@
version: 2
jobs:
install:
macos:
xcode: "9.2.0"
working_directory: ~/repo
steps:
- checkout
- restore_cache:
key: cache-{{ checksum "yarn.lock" }}
- run:
name: Installing Dependencies
command: yarn --ignore-engines
- save_cache:
key: cache-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: Getting build icon
command: if [[ $CIRCLE_BRANCH == canary ]]; then cp build/canary.icns build/icon.icns; fi
- persist_to_workspace:
root: .
paths:
- node_modules
test:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Testing
command: yarn test
build:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Building
command: yarn dist --publish 'never'
- store_artifacts:
path: dist
- persist_to_workspace:
root: .
paths:
- dist
release:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Deploying to GitHub
command: yarn dist
workflows:
version: 2
build:
jobs:
- install:
filters:
tags:
only: /.*/
- test:
requires:
- install
filters:
tags:
only: /.*/
- build:
requires:
- test
filters:
branches:
only:
- master
- canary
tags:
ignore: /.*/
- release:
requires:
- test
filters:
tags:
only: /.*/
branches:
ignore: /.*/

10
.eslintignore Normal file
View file

@ -0,0 +1,10 @@
build
app/renderer
app/static
app/bin
app/dist
app/node_modules
assets
website
bin
dist

View file

@ -18,7 +18,7 @@
- **OS version and name**: <!-- Replace with version + name -->
- **Hyper.app version**: <!-- Replace with version -->
- **Link of a [Gist](https://gist.github.com/) with the contents of your .hyper.js**: <!-- Gist Link Here -->
- **Relevant information from devtools** _(CMD+ALT+I on Mac OS, CTRL+SHIFT+I elsewhere)_: <!-- Replace with info if applicable, or N/A -->
- **Relevant information from devtools** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_: <!-- Replace with info if applicable, or N/A -->
- **The issue is reproducible in vanilla Hyper.app**: <!-- Replace with info if applicable, or `Is Vanilla`. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
## Issue

View file

@ -3,5 +3,6 @@
- To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged,
If there's anything left to do and if there are any related PRs
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site.
Thanks, again! -->

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
# build output
dist
app/renderer
bin/cli.*
# dependencies
node_modules

View file

@ -6,7 +6,7 @@ language: node_js
matrix:
include:
- os: linux
node_js: 8.4.0
node_js: 8
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
@ -25,7 +25,6 @@ addons:
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi
- npm install -g yarn
cache: yarn
@ -33,6 +32,7 @@ install:
- yarn
after_success:
- (git branch --contains $TRAVIS_COMMIT | grep canary > /dev/null || [[ "$TRAVIS_BRANCH" == "canary" ]] ) && (cd build; cp canary.icns icon.icns; cp canary.ico icon.ico)
- yarn run dist
branches:

View file

@ -1 +1 @@
save-prefix false
save-exact true

199
PLUGINS.md Normal file
View file

@ -0,0 +1,199 @@
# Plugin development
## Workflow
### Run Hyper in dev mode
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron.
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
Be sure to use the `canary` branch.
### Create a dev config file
Copy your config file `.hyper.js` to the root of your cloned repository. Hyper, in dev mode, will use this copied config file. That means that you can continue to use your main installation of Hyper with your day-to-day configuration.
After the first run, Hyper, in dev mode, will have created a new `.hyper_plugins` directory in your repository directory.
### Setup your plugin
Go to your recently created `<repository_root>/.hyper_plugins/local` directory and create/clone your plugin repo. An even better method on macOS/Linux is to add a symlink to your plugin directory.
Edit your dev config file, and add your plugin name (directory name in your `local` directory) in the `localPlugins` array.
```js
module.exports = {
config: {
...
},
plugins: [],
localPlugins: ['hyper-awesome-plugin'],
...
}
```
### Running your plugin
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.js).
After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file.
When you put a `console.log()` in your plugin code, it will be displayed in the Electron dev-tools, but only if it is located in a renderer method, like component decorators. If it is located in the Electron main process method, like the `onApp` handler, it will be displayed in your terminal where you ran `yarn run app` or in your VSCode console.
## Recipes
Almost all available API methods can be found on https://www.hyper.is.
If there's any missing, let us know or submit a PR to document it!
### Components
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierachy.
Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler.
```js
exports.decorateTerms = (Terms, {React}) => {
return class extends React.Component {
constructor(props, context) {
super(props, context);
this.terms = null;
this.onDecorated = this.onDecorated.bind(this);
}
onDecorated(terms) {
this.terms = terms;
// Don't forget to propagate it to HOC chain
if (this.props.onDecorated) this.props.onDecorated(terms);
}
render() {
return React.createElement(
Terms,
Object.assign({}, this.props, {
onDecorated: this.onDecorated
})
);
// Or if you use JSX:
// <Terms onDecorated={this.onDecorated} />
}
}
```
:warning: Note that you have to execute `this.props.onDecorated` to not break the handler chain. Without this, you could break other plugins that decorate the same component.
### Keymaps
If you want to add some keymaps, you need to do 2 things:
#### Declare your key bindings
Use the `decorateKeymaps` API handler to modify existing keymaps and add yours with the following format `command: hotkeys`.
```js
// Adding Keymaps
exports.decorateKeymaps = keymaps => {
const newKeymaps = {
'pane:maximize': 'ctrl+shift+m',
'pane:invert': 'ctrl+shift+i'
}
return Object.assign({}, keymaps, newKeymaps);
}
```
The command name can be whatever you want, but the following is better to respect the default naming convention: `<context>:<action>`.
Hotkeys are composed by (Mousetrap supported keys)[https://craig.is/killing/mice#keys].
**Bonus feature**: if your command ends with `:prefix`, it would mean that you want to use this command with an additional digit to the command. Then Hyper will create all your commands under the hood. For example, this keymap `'pane:hide:prefix': 'ctrl+shift'` will automatically generate the following:
```
{
'pane:hide:1': 'ctrl+shift+1',
'pane:hide:2': 'ctrl+shift+2',
...
'pane:hide:8': 'ctrl+shift+8',
'pane:hide:last': 'ctrl+shift+9'
}
```
Notice that `9` has been replaced by `last` because most of the time this is handy if you have more than 9 items.
#### Register a handler for your commands
##### Renderer/Window
Most of time, you'll want to execute some sort of handler in context of the renderer, like dispatching a Redux action.
To trigger these handlers, you'll have to register them with the `registerCommands` Terms method.
```js
this.terms.registerCommands({
'pane:maximize': e => {
this.props.onMaximizePane();
// e parameter is React key event
e.preventDefault();
}
})
```
##### Main process
If there is no handler in the renderer for an existing command, an `rpc` message is emitted.
If you want to execute a handler in main process you have to subscribe to a message, for example:
```js
rpc.on('command pane:snapshot', () => {
/* Awesome snapshot feature */
});
```
### Menu
Your plugin can expose a `decorateMenu` function to modify the Hyper menu template.
Check the (Electron documentation)[https://electronjs.org/docs/api/menu-item] for more details about the different menu item types/options available.
Be careful, a click handler will be executed on the main process. If you need to trigger a handler in the render process you need to use an `rpc` message like this:
```js
exports.decorateMenu = (menu) => {
debug('decorateMenu');
const isMac = process.platform === 'darwin';
// menu label is different on mac
const menuLabel = isMac ? 'Shell' : 'File';
return menu.map(menuCategory => {
if (menuCategory.label !== menuLabel) {
return menuItem;
}
return [
...menuCategory,
{
type: 'separator'
},
{
label: 'Clear all panes in all tabs',
accelerator: 'ctrl+shift+y',
click(item, focusedWindow) {
// on macOS, menu item can clicked without or minized window
if (focusedWindow) {
focusedWindow.rpc.emit('clear allPanes');
}
}
}
]
});
}
/* Plugin needs to register a rpc handler on renderer side for example in a Terms HOC*/
exports.decorateTerms = (Terms, { React }) => {
return class extends React.Component {
componentDidMount() {
window.rpc.on('clear allPanes',() => {
/* Awesome plugin feature */
})
}
}
}
```
### Cursor
If your plugin needs to know cursor position/size, it can decorate the Term component and pass a handler. This handler will be called with each cursor move while passing back all information about the cursor.
```js
exports.decorateTerm = (Term, { React, notify }) => {
// Define and return our higher order component.
return class extends React.Component {
onCursorMove (cursorFrame) {
// Don't forget to propagate it to HOC chain
if (this.props.onCursorMove) this.props.onCursorMove(cursorFrame);
const { x, y, width, height, col, row } = cursorFrame;
/* Awesome cursor feature */
}
}
}
```
## Hyper v2 breaking changes
Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY ouput renders now in a canvas element, not with a hackable DOM structure.
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.
If your plugin was deeply linked with the `hterm` API (even public methods), it certainly doesn't work anymore.
If your plugin needs some unavailable API to tweak `xterm.js`, please open an issue. We'll be happy to expose some existing `xterm.js` API or implement new ones.

50
app/auto-updater-linux.js Normal file
View file

@ -0,0 +1,50 @@
'use strict';
const fetch = require('node-fetch');
const {EventEmitter} = require('events');
class AutoUpdater extends EventEmitter {
quitAndInstall() {
this.emitError('QuitAndInstall unimplemented');
}
getFeedURL() {
return this.updateURL;
}
setFeedURL(updateURL) {
this.updateURL = updateURL;
}
checkForUpdates() {
if (!this.updateURL) {
return this.emitError('Update URL is not set');
}
this.emit('checking-for-update');
fetch(this.updateURL)
.then(res => {
if (res.status === 204) {
return this.emit('update-not-available');
}
return res.json().then(({name, notes, pub_date}) => {
// Only name is mandatory, needed to construct release URL.
if (!name) {
throw new Error('Malformed server response: release name is missing.');
}
// If `null` is passed to Date constructor, current time will be used. This doesn't work with `undefined`
const date = new Date(pub_date || null);
this.emit('update-available', {}, notes, name, date);
});
})
.catch(this.emitError.bind(this));
}
emitError(error) {
if (typeof error === 'string') {
error = new Error(error);
}
this.emit('error', error, error.message);
}
}
module.exports = new AutoUpdater();

View file

@ -1,74 +0,0 @@
// Packages
const {autoUpdater} = require('electron');
const ms = require('ms');
const retry = require('async-retry');
// Utilities
const notify = require('./notify'); // eslint-disable-line no-unused-vars
const {version} = require('./package');
const {getConfig} = require('./config');
const {platform} = process;
let isInit = false;
async function init() {
autoUpdater.on('error', (err, msg) => {
console.error('Error fetching updates', msg + ' (' + err.stack + ')');
});
const config = await retry(async () => {
const content = await getConfig();
if (!content) {
throw new Error('No config content loaded');
}
return content;
});
// Default to the "stable" update channel
let canaryUpdates = false;
// If defined in the config, switch to the "canary" channel
if (config.updateChannel && config.updateChannel === 'canary') {
canaryUpdates = true;
}
const updatePrefix = canaryUpdates ? 'releases-canary' : 'releases';
const feedURL = `https://${updatePrefix}.hyper.is/update/${platform}`;
autoUpdater.setFeedURL(`${feedURL}/${version}`);
setTimeout(() => {
autoUpdater.checkForUpdates();
}, ms('10s'));
setInterval(() => {
autoUpdater.checkForUpdates();
}, ms('30m'));
isInit = true;
}
module.exports = function (win) {
if (!isInit) {
init();
}
const {rpc} = win;
const onupdate = (ev, releaseNotes, releaseName) => {
rpc.emit('update available', {releaseNotes, releaseName});
};
autoUpdater.on('update-downloaded', onupdate);
rpc.once('quit and install', () => {
autoUpdater.quitAndInstall();
});
win.on('close', () => {
autoUpdater.removeListener('update-downloaded', onupdate);
});
};

118
app/commands.js Normal file
View file

@ -0,0 +1,118 @@
const {app} = require('electron');
const {openConfig} = require('./config');
const {updatePlugins} = require('./plugins');
const commands = {
'window:new': () => {
// If window is created on the same tick, it will consume event too
setTimeout(app.createWindow, 0);
},
'tab:new': focusedWindow => {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req');
} else {
setTimeout(app.createWindow, 0);
}
},
'pane:splitVertical': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request vertical');
},
'pane:splitHorizontal': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request horizontal');
},
'pane:close': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('termgroup close req');
},
'window:preferences': () => {
openConfig();
},
'editor:clearBuffer': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session clear req');
},
'editor:selectAll': focusedWindow => {
focusedWindow.rpc.emit('term selectAll');
},
'plugins:update': () => {
updatePlugins();
},
'window:reload': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('reload');
},
'window:reloadFull': focusedWindow => {
focusedWindow && focusedWindow.reload();
},
'window:devtools': focusedWindow => {
if (!focusedWindow) {
return;
}
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
},
'zoom:reset': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('reset fontSize req');
},
'zoom:in': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('increase fontSize req');
},
'zoom:out': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('decrease fontSize req');
},
'tab:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move left req');
},
'tab:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move right req');
},
'pane:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('prev pane req');
},
'pane:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('next pane req');
},
'editor:movePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word left req');
},
'editor:moveNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word right req');
},
'editor:moveBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line beginning req');
},
'editor:moveEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line end req');
},
'editor:deletePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word left req');
},
'editor:deleteNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word right req');
},
'editor:deleteBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line beginning req');
},
'editor:deleteEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line end req');
},
'editor:break': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session break req');
}
};
//Special numeric command
[1, 2, 3, 4, 5, 6, 7, 8, 'last'].forEach(cmdIndex => {
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
commands[`tab:jump:${cmdIndex}`] = focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move jump req', index);
};
});
exports.execCommand = (command, focusedWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

View file

@ -1,9 +1,10 @@
const fs = require('fs');
const notify = require('./notify');
const _import = require('./config/import');
const {_import, getDefaultConfig} = require('./config/import');
const _openConfig = require('./config/open');
const win = require('./config/windows');
const {cfgPath, cfgDir} = require('./config/paths');
const {getColorMap} = require('./utils/colors');
const watchers = [];
let cfg = {};
@ -15,71 +16,139 @@ const _watch = function () {
}
const onChange = () => {
// Need to wait 100ms to ensure that write is complete
setTimeout(() => {
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach(fn => fn());
checkDeprecatedConfig();
}, 100);
};
// Windows
if (process.platform === 'win32') {
// watch for changes on config every 2s on windows
// watch for changes on config every 2s on Windows
// https://github.com/zeit/hyper/pull/1772
_watcher = fs.watchFile(cfgPath, {interval: 2000}, (curr, prev) => {
if (curr.mtime === 0) {
//eslint-disable-next-line no-console
console.error('error watching config');
} else if (curr.mtime !== prev.mtime) {
onChange();
}
});
} else {
_watcher = fs.watch(cfgPath);
return;
}
// macOS/Linux
setWatcher();
function setWatcher() {
try {
_watcher = fs.watch(cfgPath, eventType => {
if (eventType === 'rename') {
_watcher.close();
// Ensure that new file has been written
setTimeout(() => setWatcher(), 500);
}
});
} catch (e) {
//eslint-disable-next-line no-console
console.error('Failed to watch config file:', cfgPath, e);
return;
}
_watcher.on('change', onChange);
_watcher.on('error', error => {
//eslint-disable-next-line no-console
console.error('error watching config', error);
});
}
};
exports.subscribe = function (fn) {
exports.subscribe = fn => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
exports.getConfigDir = function () {
exports.getConfigDir = () => {
// expose config directory to load plugin from the right place
return cfgDir;
};
exports.getConfig = function () {
exports.getConfig = () => {
return cfg.config;
};
exports.openConfig = function () {
exports.openConfig = () => {
return _openConfig();
};
exports.getPlugins = function () {
exports.getPlugins = () => {
return {
plugins: cfg.plugins,
localPlugins: cfg.localPlugins
};
};
exports.getKeymaps = function () {
exports.getKeymaps = () => {
return cfg.keymaps;
};
exports.extendKeymaps = function (keymaps) {
if (keymaps) {
cfg.keymaps = keymaps;
}
};
exports.setup = function () {
exports.setup = () => {
cfg = _import();
_watch();
checkDeprecatedConfig();
};
exports.getWin = win.get;
exports.winRecord = win.recordState;
exports.windowDefaults = win.defaults;
const getDeprecatedCSS = function(config) {
const deprecated = [];
const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection'];
deprecatedCSS.forEach(css => {
if ((config.css && config.css.indexOf(css) !== -1) || (config.termCSS && config.termCSS.indexOf(css) !== -1)) {
deprecated.push(css);
}
});
return deprecated;
};
exports.getDeprecatedCSS = getDeprecatedCSS;
const checkDeprecatedConfig = function() {
if (!cfg.config) {
return;
}
const deprecated = getDeprecatedCSS(cfg.config);
if (deprecated.length === 0) {
return;
}
const deprecatedStr = deprecated.join(', ');
notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`);
};
exports.fixConfigDefaults = decoratedConfig => {
const defaultConfig = getDefaultConfig().config;
decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {};
// We must have default colors for xterm css.
decoratedConfig.colors = Object.assign({}, defaultConfig.colors, decoratedConfig.colors);
return decoratedConfig;
};
exports.htermConfigTranslate = config => {
const cssReplacements = {
'x-screen x-row([ {.[])': '.xterm-rows > div$1',
'.cursor-node([ {.[])': '.terminal-cursor$1',
'::selection([ {.[])': '.terminal .xterm-selection div$1',
'x-screen a([ {.[])': '.terminal a$1',
'x-row a([ {.[])': '.terminal a$1'
};
Object.keys(cssReplacements).forEach(pattern => {
const searchvalue = new RegExp(pattern, 'g');
const newvalue = cssReplacements[pattern];
config.css = config.css && config.css.replace(searchvalue, newvalue);
config.termCSS = config.termCSS && config.termCSS.replace(searchvalue, newvalue);
});
return config;
};

View file

@ -4,8 +4,8 @@
module.exports = {
config: {
// Choose either "stable" for receiving highly polished,
// or "canary" for less polished but more frequent updates
// choose either `'stable'` for receiving highly polished,
// or `'canary'` for less polished but more frequent updates
updateChannel: 'stable',
// default font size in pixels for all tabs
@ -14,41 +14,53 @@ module.exports = {
// font family with optional fallbacks
fontFamily: 'Menlo, "DejaVu Sans Mono", Consolas, "Lucida Console", monospace',
// default font weight: 'normal' or 'bold'
fontWeight: 'normal',
// font weight for bold characters: 'normal' or 'bold'
fontWeightBold: 'bold',
// terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)
cursorColor: 'rgba(248,28,229,0.8)',
// `BEAM` for |, `UNDERLINE` for _, `BLOCK` for █
// terminal text color under BLOCK cursor
cursorAccentColor: '#000',
// `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █
cursorShape: 'BLOCK',
// set to true for blinking cursor
// set to `true` (without backticks and without quotes) for blinking cursor
cursorBlink: false,
// color of the text
foregroundColor: '#fff',
// terminal background color
// opacity is only supported on macOS
backgroundColor: '#000',
// terminal selection color
selectionColor: 'rgba(248,28,229,0.3)',
// border color (window, tabs)
borderColor: '#333',
// custom css to embed in the main window
// custom CSS to embed in the main window
css: '',
// custom css to embed in the terminal window
// custom CSS to embed in the terminal window
termCSS: '',
// set to `true` (without backticks and without quotes) if you're using a
// Linux setup that doesn't show native menus
// default: `false` on Linux, `true` on Windows (ignored on macOS)
// if you're using a Linux setup which show native menus, set to false
// default: `true` on Linux, `true` on Windows, ignored on macOS
showHamburgerMenu: '',
// set to `false` if you want to hide the minimize, maximize and close buttons
// set to `false` (without backticks and without quotes) if you want to hide the minimize, maximize and close buttons
// additionally, set to `'left'` if you want them on the left, like in Ubuntu
// default: `true` on windows and Linux (ignored on macOS)
// default: `true` (without backticks and without quotes) on Windows and Linux, ignored on macOS
showWindowControls: '',
// custom padding (css format, i.e.: `top right bottom left`)
// custom padding (CSS format, i.e.: `top right bottom left`)
padding: '12px 14px',
// the full list. if you're going to provide the full color palette,
@ -56,21 +68,21 @@ module.exports = {
// an array here instead of a color map object
colors: {
black: '#000000',
red: '#ff0000',
green: '#33ff00',
yellow: '#ffff00',
blue: '#0066ff',
magenta: '#cc00ff',
cyan: '#00ffff',
white: '#d0d0d0',
lightBlack: '#808080',
lightRed: '#ff0000',
lightGreen: '#33ff00',
lightYellow: '#ffff00',
lightBlue: '#0066ff',
lightMagenta: '#cc00ff',
lightCyan: '#00ffff',
lightWhite: '#ffffff'
red: '#C51E14',
green: '#1DC121',
yellow: '#C7C329',
blue: '#0A2FC4',
magenta: '#C839C5',
cyan: '#20C5C6',
white: '#C7C7C7',
lightBlack: '#686868',
lightRed: '#FD6F6B',
lightGreen: '#67F86F',
lightYellow: '#FFFA72',
lightBlue: '#6A76FB',
lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE',
lightWhite: '#FFFFFF',
},
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
@ -83,26 +95,29 @@ module.exports = {
// Bash on Windows
// - Example: `C:\\Windows\\System32\\bash.exe`
//
// Powershell on Windows
// PowerShell on Windows
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
shell: '',
// for setting shell arguments (i.e. for using interactive shellArgs: ['-i'])
// by default ['--login'] will be used
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
// by default `['--login']` will be used
shellArgs: ['--login'],
// for environment variables
env: {},
// set to false for no bell
// set to `false` for no bell
bell: 'SOUND',
// if true, selected text will automatically be copied to the clipboard
copyOnSelect: false
// if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
copyOnSelect: false,
// if true, on right click selected text will be copied or pasted if no
// selection is present (true by default on Windows)
// quickEdit: true
// if `true` (without backticks and without quotes), hyper will be set as the default protocol client for SSH
defaultSSHApp: true,
// if `true` (without backticks and without quotes), on right click selected text will be copied or pasted if no
// selection is present (`true` by default on Windows and disables the context menu feature)
// quickEdit: true,
// URL to custom bell
// bellSoundURL: 'http://example.com/bell.mp3',
@ -126,5 +141,5 @@ module.exports = {
keymaps: {
// Example
// 'window:devtools': 'cmd+alt+o',
}
},
};

View file

@ -1,8 +1,9 @@
const {writeFileSync, readFileSync} = require('fs');
const {sync: mkdirpSync} = require('mkdirp');
const {defaultCfg, cfgPath, plugs} = require('./paths');
const _init = require('./init');
const _keymaps = require('./keymaps');
const {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} = require('./paths');
const {_init, _extractDefault} = require('./init');
let defaultConfig;
const _write = function(path, data) {
// This method will take text formatted as Unix line endings and transform it
@ -21,26 +22,41 @@ const _importConf = function () {
mkdirpSync(plugs.local);
try {
const _defaultCfg = readFileSync(defaultCfg, 'utf8');
const defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
const _defaultCfg = _extractDefault(defaultCfgRaw);
// Importing platform specific keymap
try {
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
const mapping = JSON.parse(content);
_defaultCfg.keymaps = mapping;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
// Importing user config
try {
const _cfgPath = readFileSync(cfgPath, 'utf8');
return {userCfg: _cfgPath, defaultCfg: _defaultCfg};
} catch (err) {
_write(cfgPath, _defaultCfg);
return {userCfg: {}, defaultCfg: _defaultCfg};
_write(cfgPath, defaultCfgRaw);
return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg};
}
} catch (err) {
//eslint-disable-next-line no-console
console.log(err);
}
};
const _import = function () {
const cfg = _init(_importConf());
if (cfg) {
cfg.keymaps = _keymaps.import(cfg.keymaps);
}
return cfg;
exports._import = () => {
const imported = _importConf();
defaultConfig = imported.defaultCfg;
const result = _init(imported);
return result;
};
module.exports = _import;
exports.getDefaultConfig = () => {
if (!defaultConfig) {
defaultConfig = _extractDefault(_importConf().defaultCfg);
}
return defaultConfig;
};

View file

@ -1,5 +1,6 @@
const vm = require('vm');
const notify = require('../notify');
const mapKeys = require('../utils/map-keys');
const _extract = function(script) {
const module = {};
@ -14,7 +15,8 @@ const _syntaxValidation = function (cfg) {
try {
return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true});
} catch (err) {
notify(`Error loading config: ${err.name}, see DevTools for more info`);
notify('Error loading config:', `${err.name}, see DevTools for more info`);
//eslint-disable-next-line no-console
console.error('Error loading config:', err);
}
};
@ -29,19 +31,20 @@ const _init = function (cfg) {
if (script) {
const _cfg = _extract(script);
if (!_cfg.config) {
_cfg.plugins = _cfg.plugins || [];
_cfg.localPlugins = _cfg.localPlugins || [];
_cfg.keymaps = _cfg.keymaps || {};
notify('Error reading configuration: `config` key is missing');
return _extractDefault(cfg.defaultCfg);
return cfg.defaultCfg;
}
// Merging platform specific keymaps with user defined keymaps
_cfg.keymaps = mapKeys(Object.assign({}, cfg.defaultCfg.keymaps, _cfg.keymaps));
// Ignore undefined values in plugin and localPlugins array Issue #1862
_cfg.plugins = (_cfg.plugins && _cfg.plugins.filter(Boolean)) || [];
_cfg.localPlugins = (_cfg.localPlugins && _cfg.localPlugins.filter(Boolean)) || [];
return _cfg;
}
return _extractDefault(cfg.defaultCfg);
return cfg.defaultCfg;
};
module.exports = _init;
module.exports = {
_init,
_extractDefault
};

View file

@ -1,50 +0,0 @@
const {readFileSync} = require('fs');
const normalize = require('../utils/keymaps/normalize');
const {defaultPlatformKeyPath} = require('./paths');
const commands = {};
const keys = {};
const _setKeysForCommands = function (keymap) {
for (const command in keymap) {
if (command) {
commands[command] = normalize(keymap[command]);
}
}
};
const _setCommandsForKeys = function (commands) {
for (const command in commands) {
if (command) {
keys[commands[command]] = command;
}
}
};
const _import = function (customKeys) {
try {
const mapping = JSON.parse(readFileSync(defaultPlatformKeyPath()));
_setKeysForCommands(mapping);
_setKeysForCommands(customKeys);
_setCommandsForKeys(commands);
return {commands, keys};
} catch (err) {}
};
const _extend = function (customKeys) {
if (customKeys) {
for (const command in customKeys) {
if (command) {
commands[command] = normalize(customKeys[command]);
keys[normalize(customKeys[command])] = command;
}
}
}
return {commands, keys};
};
module.exports = {
import: _import,
extend: _extend
};

View file

@ -3,74 +3,74 @@ const {cfgPath} = require('./paths');
module.exports = () => Promise.resolve(shell.openItem(cfgPath));
// Windows opens .js files with WScript.exe by default
// If the user hasn't set up an editor for .js files, we fallback to notepad.
if (process.platform === 'win32') {
const Registry = require('winreg');
const {exec} = require('child_process');
// Windows opens .js files with WScript.exe by default
// If the user hasn't set up an editor for .js files, we fallback to notepad.
const getFileExtKeys = () => new Promise((resolve, reject) => {
Registry({
const getUserChoiceKey = async () => {
// Load FileExts keys for .js files
const keys = await new Promise((resolve, reject) => {
new Registry({
hive: Registry.HKCU,
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js'
})
.keys((error, keys) => {
}).keys((error, items) => {
if (error) {
reject(error);
} else {
resolve(keys || []);
resolve(items || []);
}
});
});
const hasDefaultSet = async () => {
const keys = await getFileExtKeys();
// Find UserChoice key
const userChoice = keys.filter(k => k.key.endsWith('UserChoice'));
return userChoice[0];
};
const valueGroups = await Promise.all(keys.map(key => new Promise((resolve, reject) => {
key.values((error, items) => {
const hasDefaultSet = async () => {
let userChoice = await getUserChoiceKey();
if (!userChoice) return false;
// Load key values
let values = await new Promise((resolve, reject) => {
userChoice.values((error, items) => {
if (error) {
reject(error);
}
resolve(items.map(item => item.value || '') || []);
});
})));
});
const values = valueGroups
.reduce((allValues, groupValues) => ([...allValues, ...groupValues]), [])
.filter(value => value && typeof value === 'string');
// Look for default program
const hasDefaultProgramConfigured = values.some(
value => value && typeof value === 'string' && value.endsWith('.exe') && !value.includes('WScript.exe')
);
// No default app set
if (values.length === 0) {
return false;
}
// WScript is in default apps list
if (values.some(value => value.includes('WScript.exe'))) {
const userDefaults = values.filter(value => value.endsWith('.exe') && !value.includes('WScript.exe'));
// WScript.exe is overidden
return (userDefaults.length > 0);
}
return true;
return hasDefaultProgramConfigured;
};
// This mimics shell.openItem, true if it worked, false if not.
const openNotepad = file => new Promise(resolve => {
const openNotepad = file =>
new Promise(resolve => {
exec(`start notepad.exe ${file}`, error => {
resolve(!error);
});
});
module.exports = () => hasDefaultSet()
module.exports = () =>
hasDefaultSet()
.then(yes => {
if (yes) {
return shell.openItem(cfgPath);
}
//eslint-disable-next-line no-console
console.warn('No default app set for .js files, using notepad.exe fallback');
return openNotepad(cfgPath);
})
.catch(err => {
//eslint-disable-next-line no-console
console.error('Open config with default app error:', err);
return openNotepad(cfgPath);
});

View file

@ -21,6 +21,7 @@ if (isDev) {
statSync(devCfg);
cfgPath = devCfg;
cfgDir = devDir;
//eslint-disable-next-line no-console
console.log('using config file:', cfgPath);
} catch (err) {
// ignore
@ -34,6 +35,7 @@ const plugs = {
cache: resolve(plugins, 'cache')
};
const yarn = resolve(__dirname, '../../bin/yarn-standalone.js');
const cliScriptPath = resolve(__dirname, '../../bin/hyper');
const icon = resolve(__dirname, '../static/icon96x96.png');
@ -44,13 +46,25 @@ const linuxKeys = join(keymapPath, 'linux.json');
const defaultPlatformKeyPath = () => {
switch (process.platform) {
case 'darwin': return darwinKeys;
case 'win32': return win32Keys;
case 'linux': return linuxKeys;
default: return darwinKeys;
case 'darwin':
return darwinKeys;
case 'win32':
return win32Keys;
case 'linux':
return linuxKeys;
default:
return darwinKeys;
}
};
module.exports = {
cfgDir, cfgPath, cfgFile, defaultCfg, icon, defaultPlatformKeyPath, plugs, yarn
cfgDir,
cfgPath,
cfgFile,
defaultCfg,
icon,
defaultPlatformKeyPath,
plugs,
yarn,
cliScriptPath
};

View file

@ -1,14 +1,15 @@
const Config = require('electron-config');
// local storage
const cfg = new Config({
defaults: {
const defaults = {
windowPosition: [50, 50],
windowSize: [540, 380]
}
});
};
// local storage
const cfg = new Config({defaults});
module.exports = {
defaults,
get() {
const position = cfg.get('windowPosition');
const size = cfg.get('windowSize');

View file

@ -2,8 +2,11 @@
if (['--help', '-v', '--version'].includes(process.argv[1])) {
const {version} = require('./package');
const configLocation = process.platform === 'win32' ? process.env.userprofile + '\\.hyper.js' : '~/.hyper.js';
//eslint-disable-next-line no-console
console.log(`Hyper version ${version}`);
//eslint-disable-next-line no-console
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
//eslint-disable-next-line no-console
console.log(`Hyper configuration file located at: ${configLocation}`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit();
@ -14,6 +17,7 @@ const checkSquirrel = () => {
try {
squirrel = require('electron-squirrel-startup');
//eslint-disable-next-line no-empty
} catch (err) {}
if (squirrel) {
@ -52,15 +56,16 @@ const {app, BrowserWindow, Menu} = require('electron');
const {gitDescribe} = require('git-describe');
const isDev = require('electron-is-dev');
const AppMenu = require('./menus/menu');
const config = require('./config');
// set up config
config.setup();
const plugins = require('./plugins');
const {addSymlink, addBinToUserPath} = require('./utils/cli-install');
const AppMenu = require('./menus/menu');
const Window = require('./ui/window');
const windowUtils = require('./utils/window-utils');
const windowSet = new Set([]);
@ -81,6 +86,7 @@ app.getLastFocusedWindow = () => {
};
if (isDev) {
//eslint-disable-next-line no-console
console.log('running in dev mode');
// Overide default appVersion which is set from package.json
@ -90,24 +96,31 @@ if (isDev) {
}
});
} else {
//eslint-disable-next-line no-console
console.log('running in prod mode');
if (process.platform === 'win32') {
//eslint-disable-next-line no-console
addBinToUserPath().catch(err => console.error('Failed to add Hyper CLI path to user PATH', err));
} else {
//eslint-disable-next-line no-console
addSymlink().catch(err => console.error('Failed to symlink Hyper CLI', err));
}
}
const url = 'file://' + resolve(
isDev ? __dirname : app.getAppPath(),
'index.html'
);
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');
//eslint-disable-next-line no-console
console.log('electron will open', url);
app.on('ready', () => installDevExtensions(isDev).then(() => {
app.on('ready', () =>
installDevExtensions(isDev)
.then(() => {
function createWindow(fn, options = {}) {
const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
const [width, height] = options.size ? options.size : (cfg.windowSize || winSet.size);
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
const {screen} = require('electron');
const winPos = options.position;
@ -121,10 +134,13 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
[startX, startY] = winPos;
} else if (focusedWindow) {
const points = focusedWindow.getPosition();
const currentScreen = screen.getDisplayNearestPoint({x: points[0], y: points[1]});
const currentScreen = screen.getDisplayNearestPoint({
x: points[0],
y: points[1]
});
const biggestX = ((points[0] + 100 + width) - currentScreen.bounds.x);
const biggestY = ((points[1] + 100 + height) - currentScreen.bounds.y);
const biggestX = points[0] + 100 + width - currentScreen.bounds.x;
const biggestY = points[1] + 100 + height - currentScreen.bounds.y;
if (biggestX > currentScreen.size.width) {
startX = 50;
@ -138,6 +154,10 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
}
}
if (!windowUtils.positionIsValid([startX, startY])) {
[startX, startY] = config.windowDefaults.windowPosition;
}
const hwin = new Window({width, height, x: startX, y: startY}, cfg, fn);
windowSet.add(hwin);
hwin.loadURL(url);
@ -163,6 +183,19 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
// expose to plugins
app.createWindow = createWindow;
if (!isDev) {
// check if should be set/removed as default ssh protocol client
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Setting Hyper as default client for ssh:// protocol');
app.setAsDefaultProtocolClient('ssh');
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Removing Hyper from default client for ssh:// protocl');
app.removeAsDefaultProtocolClient('ssh');
}
}
// mac only. when the dock icon is clicked
// and we don't have any active windows open,
// we open one
@ -173,40 +206,34 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
});
const makeMenu = () => {
const menu = plugins.decorateMenu(
AppMenu(createWindow, () => {
plugins.updatePlugins({force: true});
},
plugins.getLoadedPluginVersions
));
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
// If we're on Mac make a Dock Menu
if (process.platform === 'darwin') {
const dockMenu = Menu.buildFromTemplate([{
const dockMenu = Menu.buildFromTemplate([
{
label: 'New Window',
click() {
createWindow();
}
}]);
}
]);
app.dock.setMenu(dockMenu);
}
Menu.setApplicationMenu(
Menu.buildFromTemplate(menu)
);
Menu.setApplicationMenu(AppMenu.buildMenu(menu));
};
const load = () => {
plugins.onApp(app);
plugins.extendKeymaps();
makeMenu();
};
load();
plugins.subscribe(load);
}).catch(err => {
plugins.subscribe(plugins.onApp.bind(undefined, app));
config.subscribe(makeMenu);
})
.catch(err => {
//eslint-disable-next-line no-console
console.error('Error while loading devtools extensions', err);
}));
})
);
app.on('open-file', (event, path) => {
const lastWindow = app.getLastFocusedWindow();
@ -222,17 +249,28 @@ app.on('open-file', (event, path) => {
}
});
function installDevExtensions(isDev) {
if (!isDev) {
app.on('open-url', (event, sshUrl) => {
const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open ssh', sshUrl);
if (lastWindow) {
callback(lastWindow);
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
app.createWindow(callback);
} else {
// If createWindow doesn't exist yet ('ready' event was not fired),
// sets his callback to an app.windowCallback property.
app.windowCallback = callback;
}
});
function installDevExtensions(isDev_) {
if (!isDev_) {
return Promise.resolve();
}
// eslint-disable-next-line import/no-extraneous-dependencies
const installer = require('electron-devtools-installer');
const extensions = [
'REACT_DEVELOPER_TOOLS',
'REDUX_DEVTOOLS'
];
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload)));

View file

@ -1,31 +1,51 @@
{
"window:devtools": "cmd+alt+i",
"window:reload": "cmd+r",
"window:reloadFull": "cmd+shift+r",
"window:preferences": "cmd+,",
"zoom:reset": "cmd+0",
"zoom:in": "cmd+plus",
"zoom:out": "cmd+-",
"window:new": "cmd+n",
"window:minimize": "cmd+m",
"window:zoom": "ctrl+alt+cmd+m",
"window:toggleFullScreen": "cmd+ctrl+f",
"window:close": "cmd+shift+w",
"tab:new": "cmd+t",
"tab:next": "cmd+shift+]",
"tab:prev": "cmd+shift+[",
"pane:next": "cmd+]",
"pane:prev": "cmd+[",
"pane:splitVertical": "cmd+d",
"pane:splitHorizontal": "cmd+shift+d",
"pane:close": "cmd+w",
"editor:undo": "cmd+z",
"editor:redo": "cmd+y",
"editor:cut": "cmd+x",
"editor:copy": "cmd+c",
"editor:paste": "cmd+v",
"editor:selectAll": "cmd+a",
"editor:clearBuffer": "cmd+k",
"editor:emojis": "cmd+ctrl+space",
"plugins:update": "cmd+shift+u"
"window:devtools": "command+alt+i",
"window:reload": "command+r",
"window:reloadFull": "command+shift+r",
"window:preferences": "command+,",
"zoom:reset": "command+0",
"zoom:in": "command+plus",
"zoom:out": "command+-",
"window:new": "command+n",
"window:minimize": "command+m",
"window:zoom": "ctrl+alt+command+m",
"window:toggleFullScreen": "command+ctrl+f",
"window:close": "command+shift+w",
"tab:new": "command+t",
"tab:next": [
"command+shift+]",
"command+shift+right",
"command+alt+right",
"ctrl+tab"
],
"tab:prev": [
"command+shift+[",
"command+shift+left",
"command+alt+left",
"ctrl+shift+tab"
],
"tab:jump:prefix": "command",
"pane:next": "command+]",
"pane:prev": "command+[",
"pane:splitVertical": "command+d",
"pane:splitHorizontal": "command+shift+d",
"pane:close": "command+w",
"editor:undo": "command+z",
"editor:redo": "command+y",
"editor:cut": "command+x",
"editor:copy": "command+c",
"editor:paste": "command+v",
"editor:selectAll": "command+a",
"editor:movePreviousWord": "alt+left",
"editor:moveNextWord": "alt+right",
"editor:moveBeginningLine": "command+left",
"editor:moveEndLine": "command+right",
"editor:deletePreviousWord": "alt+backspace",
"editor:deleteNextWord": "alt+delete",
"editor:deleteBeginningLine": "command+backspace",
"editor:deleteEndLine": "command+delete",
"editor:clearBuffer": "command+k",
"editor:emojis": "command+ctrl+space",
"editor:break": "ctrl+c",
"plugins:update": "command+shift+u"
}

View file

@ -12,8 +12,19 @@
"window:toggleFullScreen": "f11",
"window:close": "ctrl+shift+q",
"tab:new": "ctrl+shift+t",
"tab:next": "ctrl+tab",
"tab:prev": "ctrl+shift+tab",
"tab:next": [
"ctrl+shift+]",
"ctrl+shift+right",
"ctrl+alt+right",
"ctrl+tab"
],
"tab:prev": [
"ctrl+shift+[",
"ctrl+shift+left",
"ctrl+alt+left",
"ctrl+shift+tab"
],
"tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d",
@ -25,6 +36,15 @@
"editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a",
"editor:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "home",
"editor:moveEndLine": "end",
"editor:deletePreviousWord": "ctrl+backspace",
"editor:deleteNextWord": "ctrl+del",
"editor:deleteBeginningLine": "ctrl+home",
"editor:deleteEndLine": "ctrl+end",
"editor:clearBuffer": "ctrl+shift+k",
"editor:break": "ctrl+c",
"plugins:update": "ctrl+shift+u"
}

View file

@ -7,13 +7,27 @@
"zoom:in": "ctrl+plus",
"zoom:out": "ctrl+-",
"window:new": "ctrl+shift+n",
"window:minimize": "ctrl+m",
"window:minimize": "ctrl+shift+m",
"window:zoom": "ctrl+shift+alt+m",
"window:toggleFullScreen": "f11",
"window:close": "ctrl+shift+q",
"window:close": [
"ctrl+shift+q",
"alt+f4"
],
"tab:new": "ctrl+shift+t",
"tab:next": "ctrl+tab",
"tab:prev": "ctrl+shift+tab",
"tab:next": [
"ctrl+shift+]",
"ctrl+shift+right",
"ctrl+alt+right",
"ctrl+tab"
],
"tab:prev": [
"ctrl+shift+[",
"ctrl+shift+left",
"ctrl+alt+left",
"ctrl+shift+tab"
],
"tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d",
@ -25,6 +39,15 @@
"editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a",
"editor:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "home",
"editor:moveEndLine": "end",
"editor:deletePreviousWord": "ctrl+backspace",
"editor:deleteNextWord": "ctrl+del",
"editor:deleteBeginningLine": "ctrl+home",
"editor:deleteEndLine": "ctrl+end",
"editor:clearBuffer": "ctrl+shift+k",
"editor:break": "ctrl+c",
"plugins:update": "ctrl+shift+u"
}

View file

@ -1,8 +1,8 @@
// Packages
const {app, dialog} = require('electron');
const {app, dialog, Menu} = require('electron');
// Utilities
const {getKeymaps, getConfig} = require('../config');
const {getConfig} = require('../config');
const {icon} = require('../config/paths');
const viewMenu = require('./menus/view');
const shellMenu = require('./menus/shell');
@ -11,13 +11,22 @@ const pluginsMenu = require('./menus/plugins');
const windowMenu = require('./menus/window');
const helpMenu = require('./menus/help');
const darwinMenu = require('./menus/darwin');
const {getDecoratedKeymaps} = require('../plugins');
const {execCommand} = require('../commands');
const appName = app.getName();
const appVersion = app.getVersion();
module.exports = (createWindow, updatePlugins, getLoadedPluginVersions) => {
let menu_ = [];
exports.createMenu = (createWindow, getLoadedPluginVersions) => {
const config = getConfig();
const {commands} = getKeymaps();
// We take only first shortcut in array for each command
const allCommandKeys = getDecoratedKeymaps();
const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => {
result[command] = allCommandKeys[command][0];
return result;
}, {});
let updateChannel = 'stable';
@ -27,27 +36,31 @@ module.exports = (createWindow, updatePlugins, getLoadedPluginVersions) => {
const showAbout = () => {
const loadedPlugins = getLoadedPluginVersions();
const pluginList = loadedPlugins.length === 0 ?
'none' :
loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`);
const pluginList =
loadedPlugins.length === 0 ? 'none' : loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`);
dialog.showMessageBox({
title: `About ${appName}`,
message: `${appName} ${appVersion} (${updateChannel})`,
detail: `Plugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2017 Zeit, Inc.`,
detail: `Plugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2018 ZEIT, Inc.`,
buttons: [],
icon
});
};
const menu = [
...(process.platform === 'darwin' ? [darwinMenu(commands, showAbout)] : []),
shellMenu(commands, createWindow),
editMenu(commands),
viewMenu(commands),
pluginsMenu(commands, updatePlugins),
windowMenu(commands),
helpMenu(commands, showAbout)
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
shellMenu(commandKeys, execCommand),
editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand),
pluginsMenu(commandKeys, execCommand),
windowMenu(commandKeys, execCommand),
helpMenu(commandKeys, showAbout)
];
return menu;
};
exports.buildMenu = template => {
menu_ = Menu.buildFromTemplate(template);
return menu_;
};

View file

@ -1,9 +1,8 @@
// This menu label is overrided by OSX to be the appName
// The label is set to appName here so it matches actual behavior
const {app} = require('electron');
const {openConfig} = require('../../config');
module.exports = function (commands, showAbout) {
module.exports = (commandKeys, execCommand, showAbout) => {
return {
label: `${app.getName()}`,
submenu: [
@ -18,9 +17,9 @@ module.exports = function (commands, showAbout) {
},
{
label: 'Preferences...',
accelerator: commands['window:preferences'],
accelerator: commandKeys['window:preferences'],
click() {
openConfig();
execCommand('window:preferences');
}
},
{

View file

@ -1,44 +1,116 @@
const {openConfig} = require('../../config');
module.exports = function (commands) {
module.exports = (commandKeys, execCommand) => {
const submenu = [
{
role: 'undo',
accelerator: commands['editor:undo']
label: 'Undo',
accelerator: commandKeys['editor:undo'],
enabled: false
},
{
role: 'redo',
accelerator: commands['editor:redo']
label: 'Redo',
accelerator: commandKeys['editor:redo'],
enabled: false
},
{
type: 'separator'
},
{
role: 'cut',
accelerator: commands['editor:cut']
label: 'Cut',
accelerator: commandKeys['editor:cut'],
enabled: false
},
{
role: 'copy',
accelerator: commands['editor:copy']
command: 'editor:copy',
accelerator: commandKeys['editor:copy']
},
{
role: 'paste',
accelerator: commands['editor:paste']
accelerator: commandKeys['editor:paste']
},
{
role: 'selectall',
accelerator: commands['editor:selectAll']
label: 'Select All',
accelerator: commandKeys['editor:selectAll'],
click(item, focusedWindow) {
execCommand('editor:selectAll', focusedWindow);
}
},
{
type: 'separator'
},
{
label: 'Move to...',
submenu: [
{
label: 'Previous word',
accelerator: commandKeys['editor:movePreviousWord'],
click(item, focusedWindow) {
execCommand('editor:movePreviousWord', focusedWindow);
}
},
{
label: 'Next word',
accelerator: commandKeys['editor:moveNextWord'],
click(item, focusedWindow) {
execCommand('editor:moveNextWord', focusedWindow);
}
},
{
label: 'Line beginning',
accelerator: commandKeys['editor:moveBeginningLine'],
click(item, focusedWindow) {
execCommand('editor:moveBeginningLine', focusedWindow);
}
},
{
label: 'Line end',
accelerator: commandKeys['editor:moveEndLine'],
click(item, focusedWindow) {
execCommand('editor:moveEndLine', focusedWindow);
}
}
]
},
{
label: 'Delete...',
submenu: [
{
label: 'Previous word',
accelerator: commandKeys['editor:deletePreviousWord'],
click(item, focusedWindow) {
execCommand('editor:deletePreviousWord', focusedWindow);
}
},
{
label: 'Next word',
accelerator: commandKeys['editor:deleteNextWord'],
click(item, focusedWindow) {
execCommand('editor:deleteNextWord', focusedWindow);
}
},
{
label: 'Line beginning',
accelerator: commandKeys['editor:deleteBeginningLine'],
click(item, focusedWindow) {
execCommand('editor:deleteBeginningLine', focusedWindow);
}
},
{
label: 'Line end',
accelerator: commandKeys['editor:deleteEndLine'],
click(item, focusedWindow) {
execCommand('editor:deleteEndLine', focusedWindow);
}
}
]
},
{
type: 'separator'
},
{
label: 'Clear Buffer',
accelerator: commands['editor:clearBuffer'],
accelerator: commandKeys['editor:clearBuffer'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('session clear req');
}
execCommand('editor:clearBuffer', focusedWindow);
}
}
];
@ -48,9 +120,9 @@ module.exports = function (commands) {
{type: 'separator'},
{
label: 'Preferences...',
accelerator: commands['window:preferences'],
accelerator: commandKeys['window:preferences'],
click() {
openConfig();
execCommand('window:preferences');
}
}
);

View file

@ -1,7 +1,11 @@
const os = require('os');
const {release} = require('os');
const {app, shell} = require('electron');
module.exports = function (commands, showAbout) {
const {getConfig, getPlugins} = require('../../config');
const {arch, env, platform, versions} = process;
const {version} = require('../../package.json');
module.exports = (commands, showAbout) => {
const submenu = [
{
label: `${app.getName()} Website`,
@ -13,11 +17,48 @@ module.exports = function (commands, showAbout) {
label: 'Report Issue',
click() {
const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. -->
-
${app.getName()} ${app.getVersion()}
Electron ${process.versions.electron}
${process.platform} ${process.arch} ${os.release()}`;
<!--
Hi there! Thank you for discovering and submitting an issue.
Before you submit this; let's make sure of a few things.
Please make sure the following boxes are if they are correct.
If not, please try and fulfil these first.
-->
<!-- 👉 Checked checkbox should look like this: [x] -->
- [ ] Your Hyper.app version is **${version}**. Please verify your using the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
---
- **Any relevant information from devtools?** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_:
<!-- 👉 Replace with info if applicable, or N/A -->
- **Is the issue reproducible in vanilla Hyper.app?**
<!-- 👉 Replace with info if applicable, or Is Vanilla. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
## Issue
<!-- 👉 Now feel free to write your issue, but please be descriptive! Thanks again 🙌 -->
<!-- ~/.hyper.js config -->
- **${app.getName()} version**: ${env.TERM_PROGRAM_VERSION} "${app.getVersion()}"
- **OS ARCH VERSION:** ${platform} ${arch} ${release()}
- **Electron:** ${versions.electron} **LANG:** ${env.LANG}
- **SHELL:** ${env.SHELL} **TERM:** ${env.TERM}
<details>
<summary><strong> ~/.hyper.js contents</strong></summary>
<pre>
<code>
${JSON.stringify(getConfig(), null, 2)}
${JSON.stringify(getPlugins(), null, 2)}
</code>
</pre>
</details>`;
shell.openExternal(`https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(body)}`);
}
@ -35,7 +76,6 @@ module.exports = function (commands, showAbout) {
}
);
}
return {
role: 'help',
submenu

View file

@ -1,4 +1,4 @@
module.exports = function (commands, update) {
module.exports = (commands, update) => {
return {
label: 'Plugins',
submenu: [

View file

@ -1,64 +1,54 @@
module.exports = function (commands, createWindow) {
module.exports = (commandKeys, execCommand) => {
const isMac = process.platform === 'darwin';
return {
label: isMac ? 'Shell' : 'File',
submenu: [
{
label: 'New Window',
accelerator: commands['window:new'],
click() {
createWindow();
label: 'New Tab',
accelerator: commandKeys['tab:new'],
click(item, focusedWindow) {
execCommand('tab:new', focusedWindow);
}
},
{
label: 'New Tab',
accelerator: commands['tab:new'],
label: 'New Window',
accelerator: commandKeys['window:new'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req');
} else {
createWindow();
}
execCommand('window:new', focusedWindow);
}
},
{
type: 'separator'
},
{
label: 'Split Vertically',
accelerator: commands['pane:splitVertical'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request vertical');
}
}
},
{
label: 'Split Horizontally',
accelerator: commands['pane:splitHorizontal'],
accelerator: commandKeys['pane:splitHorizontal'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request horizontal');
execCommand('pane:splitHorizontal', focusedWindow);
}
},
{
label: 'Split Vertically',
accelerator: commandKeys['pane:splitVertical'],
click(item, focusedWindow) {
execCommand('pane:splitVertical', focusedWindow);
}
},
{
type: 'separator'
},
{
label: 'Close Session',
accelerator: commands['pane:close'],
label: 'Close',
accelerator: commandKeys['pane:close'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup close req');
}
execCommand('pane:close', focusedWindow);
}
},
{
label: isMac ? 'Close Window' : 'Quit',
role: 'close',
accelerator: commands['window:close']
accelerator: commandKeys['window:close']
}
]
};

View file

@ -1,37 +1,26 @@
module.exports = function (commands) {
module.exports = (commandKeys, execCommand) => {
return {
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: commands['window:reload'],
accelerator: commandKeys['window:reload'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reload');
}
execCommand('window:reload', focusedWindow);
}
},
{
label: 'Full Reload',
accelerator: commands['window:reloadFull'],
accelerator: commandKeys['window:reloadFull'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload();
}
execCommand('window:reloadFull', focusedWindow);
}
},
{
label: 'Developer Tools',
accelerator: commands['window:devtools'],
click(item, focusedWindow) {
if (focusedWindow) {
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
}
accelerator: commandKeys['window:devtools'],
click: (item, focusedWindow) => {
execCommand('window:devtools', focusedWindow);
}
},
{
@ -39,29 +28,23 @@ module.exports = function (commands) {
},
{
label: 'Reset Zoom Level',
accelerator: commands['zoom:reset'],
accelerator: commandKeys['zoom:reset'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reset fontSize req');
}
execCommand('zoom:reset', focusedWindow);
}
},
{
label: 'Zoom In',
accelerator: commands['zoom:in'],
accelerator: commandKeys['zoom:in'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('increase fontSize req');
}
execCommand('zoom:in', focusedWindow);
}
},
{
label: 'Zoom Out',
accelerator: commands['zoom:out'],
accelerator: commandKeys['zoom:out'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('decrease fontSize req');
}
execCommand('zoom:out', focusedWindow);
}
}
]

View file

@ -1,39 +1,51 @@
module.exports = function (commands) {
module.exports = (commandKeys, execCommand) => {
// Generating tab:jump array
const tabJump = [];
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const label = i === 9 ? 'Last' : `${i}`;
tabJump.push({
label: label,
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
});
}
return {
role: 'window',
submenu: [
{
role: 'minimize',
accelerator: commands['window:minimize']
accelerator: commandKeys['window:minimize']
},
{
type: 'separator'
},
{ // It's the same thing as clicking the green traffc-light on macOS
{
// It's the same thing as clicking the green traffc-light on macOS
role: 'zoom',
accelerator: commands['window:zoom']
accelerator: commandKeys['window:zoom']
},
{
label: 'Select Tab',
submenu: [
{
label: 'Previous',
accelerator: commands['tab:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move left req');
}
accelerator: commandKeys['tab:prev'],
click: (item, focusedWindow) => {
execCommand('tab:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commands['tab:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move right req');
}
}
accelerator: commandKeys['tab:next'],
click: (item, focusedWindow) => {
execCommand('tab:next', focusedWindow);
}
},
{
type: 'separator'
},
...tabJump
]
},
{
@ -44,20 +56,16 @@ module.exports = function (commands) {
submenu: [
{
label: 'Previous',
accelerator: commands['pane:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('prev pane req');
}
accelerator: commandKeys['pane:prev'],
click: (item, focusedWindow) => {
execCommand('pane:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commands['pane:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('next pane req');
}
accelerator: commandKeys['pane:next'],
click: (item, focusedWindow) => {
execCommand('pane:next', focusedWindow);
}
}
]
@ -70,7 +78,7 @@ module.exports = function (commands) {
},
{
role: 'togglefullscreen',
accelerators: commands['window:toggleFullScreen']
accelerators: commandKeys['window:toggleFullScreen']
}
]
};

View file

@ -10,10 +10,11 @@ module.exports = function fetchNotifications(win) {
const retry = err => {
setTimeout(() => fetchNotifications(win), ms('30m'));
if (err) {
//eslint-disable-next-line no-console
console.error('Notification messages fetch error', err.stack);
}
};
//eslint-disable-next-line no-console
console.log('Checking for notification messages');
fetch(NEWS_URL, {
headers: {
@ -28,6 +29,7 @@ module.exports = function fetchNotifications(win) {
throw new Error('Bad response');
}
if (message === '') {
//eslint-disable-next-line no-console
console.log('No matching notification messages');
} else {
rpc.emit('add notification', message);

View file

@ -16,10 +16,7 @@ app.on('ready', () => {
const win_ = new BrowserWindow({
show: false
});
const url = 'file://' + resolve(
isDev ? __dirname : app.getAppPath(),
'notify.html'
);
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'notify.html');
win_.loadURL(url);
win_.webContents.on('dom-ready', () => {
win = win_;
@ -31,6 +28,7 @@ app.on('ready', () => {
});
function notify(title, body) {
//eslint-disable-next-line no-console
console.log(`[Notification] ${title}: ${body}`);
if (win) {
win.webContents.send('notification', {title, body});

View file

@ -2,17 +2,16 @@
"name": "hyper",
"productName": "Hyper",
"description": "A terminal built on web technologies",
"version": "1.4.8",
"version": "2.0.0-canary.17",
"license": "MIT",
"author": {
"name": "Zeit, Inc.",
"name": "ZEIT, Inc.",
"email": "team@zeit.co"
},
"repository": "zeit/hyper",
"xo": false,
"dependencies": {
"async-retry": "1.1.3",
"color": "2.0.0",
"async-retry": "1.1.4",
"color": "2.0.1",
"convert-css-color-name-to-hex": "0.1.1",
"default-shell": "1.0.1",
"electron-config": "1.0.0",
@ -20,15 +19,16 @@
"electron-squirrel-startup": "1.0.0",
"file-uri-to-path": "1.0.0",
"git-describe": "4.0.2",
"lodash": "4.17.4",
"lodash": "4.17.5",
"mkdirp": "0.5.1",
"ms": "2.0.0",
"node-fetch": "1.7.2",
"node-pty": "0.7.0",
"queue": "4.4.0",
"semver": "5.4.1",
"ms": "2.1.1",
"node-fetch": "1.7.3",
"node-pty": "0.7.4",
"parse-url": "3.0.2",
"queue": "4.4.2",
"semver": "5.5.0",
"shell-env": "0.3.0",
"uuid": "3.1.0",
"uuid": "3.2.1",
"winreg": "1.2.4"
}
}

View file

@ -6,10 +6,10 @@ const ms = require('ms');
const config = require('./config');
const notify = require('./notify');
const _keys = require('./config/keymaps');
const {availableExtensions} = require('./plugins/extensions');
const {install} = require('./plugins/install');
const {plugs} = require('./config/paths');
const mapKeys = require('./utils/map-keys');
// local storage
const cache = new Config();
@ -19,7 +19,7 @@ const localPath = plugs.local;
// caches
let plugins = config.getPlugins();
let paths = getPaths(plugins);
let paths = getPaths();
let id = getId(plugins);
let modules = requirePlugins();
@ -43,6 +43,15 @@ config.subscribe(() => {
}
});
function checkDeprecatedExtendKeymaps() {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
return;
}
});
}
let updating = false;
function updatePlugins({force = false} = {}) {
@ -56,17 +65,14 @@ function updatePlugins({force = false} = {}) {
updating = false;
if (err) {
console.error(err.stack);
notify(
'Error updating plugins.',
err.message
);
//eslint-disable-next-line no-console
notify('Error updating plugins.', err);
} else {
// flag successful plugin update
cache.set('hyper.plugins', id_);
// cache paths
paths = getPaths(plugins);
paths = getPaths();
// clear require cache
clearCache();
@ -81,19 +87,15 @@ function updatePlugins({force = false} = {}) {
cache.set('hyper.plugin-versions', pluginVersions);
// notify watchers
watchers.forEach(fn => fn(err, {force}));
if (force || changed) {
if (changed) {
notify(
'Plugins Updated',
'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!'
);
notify('Plugins Updated', 'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!');
} else {
notify(
'Plugins Updated',
'No changes!'
);
notify('Plugins Updated', 'No changes!');
}
watchers.forEach(fn => fn(err, {force}));
checkDeprecatedExtendKeymaps();
}
}
});
@ -101,16 +103,14 @@ function updatePlugins({force = false} = {}) {
function getPluginVersions() {
const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map(path => {
return paths_.map(path_ => {
let version = null;
try {
//eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path, 'package.json')).version;
version = require(resolve(path_, 'package.json')).version;
//eslint-disable-next-line no-empty
} catch (err) {}
return [
basename(path),
version
];
return [basename(path_), version];
});
}
@ -132,7 +132,7 @@ function clearCache() {
exports.updatePlugins = updatePlugins;
exports.getLoadedPluginVersions = function () {
exports.getLoadedPluginVersions = () => {
return modules.map(mod => ({name: mod._name, version: mod._version}));
};
@ -141,10 +141,11 @@ exports.getLoadedPluginVersions = function () {
// to prevent slowness
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
// install immediately if the user changed plugins
//eslint-disable-next-line no-console
console.log('plugins have changed / not init, scheduling plugins installation');
setTimeout(() => {
updatePlugins();
}, 5000);
}, 1000);
}
// otherwise update plugins every 5 hours
@ -178,9 +179,9 @@ function alert(message) {
});
}
function toDependencies(plugins) {
function toDependencies(plugins_) {
const obj = {};
plugins.plugins.forEach(plugin => {
plugins_.plugins.forEach(plugin => {
const regex = /.(@|#)/;
const match = regex.exec(plugin);
@ -198,7 +199,7 @@ function toDependencies(plugins) {
return obj;
}
exports.subscribe = function (fn) {
exports.subscribe = fn => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
@ -208,7 +209,7 @@ exports.subscribe = function (fn) {
function getPaths() {
return {
plugins: plugins.plugins.map(name => {
return resolve(path, 'node_modules', name.split('#')[0]);
return resolve(path, 'node_modules', name.split('#')[0].split('@')[0]);
}),
localPlugins: plugins.localPlugins.map(name => {
return resolve(localPath, name);
@ -220,59 +221,75 @@ function getPaths() {
exports.getPaths = getPaths;
// get paths from renderer
exports.getBasePaths = function () {
exports.getBasePaths = () => {
return {path, localPath};
};
function requirePlugins() {
const {plugins, localPlugins} = paths;
const {plugins: plugins_, localPlugins} = paths;
const load = path => {
const load = path_ => {
let mod;
try {
// eslint-disable-next-line import/no-dynamic-require
mod = require(path);
mod = require(path_);
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
if (!exposed) {
notify('Plugin error!', `Plugin "${basename(path)}" does not expose any ` +
'Hyper extension API methods');
notify('Plugin error!', `Plugin "${basename(path_)}" does not expose any ` + 'Hyper extension API methods');
return;
}
// populate the name for internal errors here
mod._name = basename(path);
mod._name = basename(path_);
try {
// eslint-disable-next-line import/no-dynamic-require
mod._version = require(resolve(path, 'package.json')).version;
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
console.warn(`No package.json found in ${path}`);
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path_}`);
}
//eslint-disable-next-line no-console
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
return mod;
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
//eslint-disable-next-line no-console
console.warn(`Plugin "${basename(path_)}" not found: ${path_}`);
} else {
//eslint-disable-next-line no-console
console.error(err);
notify('Plugin error!', `Plugin "${basename(path)}" failed to load (${err.message})`);
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`);
}
}
};
return plugins.map(load)
return plugins_
.map(load)
.concat(localPlugins.map(load))
.filter(v => Boolean(v));
}
exports.onApp = function (app) {
exports.onApp = app_ => {
modules.forEach(plugin => {
if (plugin.onApp) {
plugin.onApp(app);
try {
plugin.onApp(app_);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`);
}
}
});
};
exports.onWindow = function (win) {
exports.onWindow = win => {
modules.forEach(plugin => {
if (plugin.onWindow) {
try {
plugin.onWindow(win);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`);
}
}
});
};
@ -283,7 +300,13 @@ function decorateObject(base, key) {
let decorated = base;
modules.forEach(plugin => {
if (plugin[key]) {
const res = plugin[key](decorated);
let res;
try {
res = plugin[key](decorated);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" when decorating ${key}`);
return;
}
if (res && typeof res === 'object') {
decorated = res;
} else {
@ -295,29 +318,54 @@ function decorateObject(base, key) {
return decorated;
}
exports.extendKeymaps = function () {
exports.getDeprecatedConfig = () => {
const deprecated = {};
const baseConfig = config.getConfig();
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
const keys = _keys.extend(plugin.extendKeymaps());
config.extendKeymaps(keys);
if (!plugin.decorateConfig) {
return;
}
// We need to clone config in case of plugin modifies config directly.
let configTmp;
try {
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`);
return;
}
const pluginCSSDeprecated = config.getDeprecatedCSS(configTmp);
if (pluginCSSDeprecated.length === 0) {
return;
}
deprecated[plugin._name] = {css: pluginCSSDeprecated};
});
return deprecated;
};
exports.decorateMenu = function (tpl) {
exports.decorateMenu = tpl => {
return decorateObject(tpl, 'decorateMenu');
};
exports.getDecoratedEnv = function (baseEnv) {
exports.getDecoratedEnv = baseEnv => {
return decorateObject(baseEnv, 'decorateEnv');
};
exports.getDecoratedConfig = function () {
exports.getDecoratedConfig = () => {
const baseConfig = config.getConfig();
return decorateObject(baseConfig, 'decorateConfig');
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig);
return translatedConfig;
};
exports.getDecoratedBrowserOptions = function (defaults) {
exports.getDecoratedKeymaps = () => {
const baseKeymaps = config.getKeymaps();
// Ensure that all keys are in an array and don't use deprecated key combination`
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
return decoratedKeymaps;
};
exports.getDecoratedBrowserOptions = defaults => {
return decorateObject(defaults, 'decorateBrowserOptions');
};

View file

@ -1,18 +1,40 @@
module.exports = {
availableExtensions: new Set([
'onApp', 'onWindow', 'onRendererWindow', 'onUnload', 'middleware',
'reduceUI', 'reduceSessions', 'reduceTermGroups',
'decorateMenu', 'decorateTerm', 'decorateHyper',
'onApp',
'onWindow',
'onRendererWindow',
'onUnload',
'middleware',
'reduceUI',
'reduceSessions',
'reduceTermGroups',
'decorateBrowserOptions',
'decorateMenu',
'decorateTerm',
'decorateHyper',
'decorateHyperTerm', // for backwards compatibility with hyperterm
'decorateHeader', 'decorateTerms', 'decorateTab',
'decorateNotification', 'decorateNotifications',
'decorateTabs', 'decorateConfig', 'decorateEnv',
'decorateTermGroup', 'decorateSplitPane', 'getTermProps',
'getTabProps', 'getTabsProps', 'getTermGroupProps',
'mapHyperTermState', 'mapTermsState',
'mapHeaderState', 'mapNotificationsState',
'mapHyperTermDispatch', 'mapTermsDispatch',
'mapHeaderDispatch', 'mapNotificationsDispatch',
'extendKeymaps'
'decorateHeader',
'decorateTerms',
'decorateTab',
'decorateNotification',
'decorateNotifications',
'decorateTabs',
'decorateConfig',
'decorateKeymaps',
'decorateEnv',
'decorateTermGroup',
'decorateSplitPane',
'getTermProps',
'getTabProps',
'getTabsProps',
'getTermGroupProps',
'mapHyperTermState',
'mapTermsState',
'mapHeaderState',
'mapNotificationsState',
'mapHyperTermDispatch',
'mapTermsDispatch',
'mapHeaderDispatch',
'mapNotificationsDispatch'
])
};

View file

@ -13,23 +13,28 @@ module.exports = {
};
spawnQueue.push(end => {
const cmd = [process.execPath, yarn].concat(args).join(' ');
//eslint-disable-next-line no-console
console.log('Launching yarn:', cmd);
cp.execFile(process.execPath, [yarn].concat(args), {
cp.execFile(
process.execPath,
[yarn].concat(args),
{
cwd: plugs.base,
env,
timeout: ms('5m'),
maxBuffer: 1024 * 1024
}, err => {
},
(err, stdout, stderr) => {
if (err) {
cb(err);
cb(stderr);
} else {
cb(null);
}
end();
spawnQueue.start();
});
}
);
});
spawnQueue.start();

View file

@ -3,7 +3,6 @@ const {ipcMain} = require('electron');
const uuid = require('uuid');
class Server extends EventEmitter {
constructor(win) {
super();
this.win = win;
@ -48,7 +47,6 @@ class Server extends EventEmitter {
this.destroyed = true;
}
}
}
module.exports = win => {

View file

@ -8,7 +8,10 @@ const {getDecoratedEnv} = require('./plugins');
const {productName, version} = require('./package');
const config = require('./config');
const createNodePtyError = () => new Error('`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.');
const createNodePtyError = () =>
new Error(
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
);
let spawn;
try {
@ -20,15 +23,20 @@ try {
const envFromConfig = config.getConfig().env || {};
module.exports = class Session extends EventEmitter {
constructor({rows, cols: columns, cwd, shell, shellArgs}) {
super();
const baseEnv = Object.assign({}, process.env, {
const baseEnv = Object.assign(
{},
process.env,
{
LANG: app.getLocale().replace('-', '_') + '.UTF-8',
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: productName,
TERM_PROGRAM_VERSION: version
}, envFromConfig);
},
envFromConfig
);
// Electron has a default value for process.env.GOOGLE_API_KEY
// We don't want to leak this to the shell
@ -85,6 +93,7 @@ module.exports = class Session extends EventEmitter {
try {
this.pty.resize(cols, rows);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
}
}
@ -93,10 +102,10 @@ module.exports = class Session extends EventEmitter {
try {
this.pty.kill();
} catch (err) {
//eslint-disable-next-line no-console
console.error('exit error', err.stack);
}
this.emit('exit');
this.ended = true;
}
};

View file

@ -9,17 +9,20 @@ const regParts = [
];
function addValues(hyperKey, commandKey, callback) {
hyperKey.set(regParts[1].name, Registry.REG_SZ, regParts[1].value, err => {
if (err) {
console.error(err.message);
hyperKey.set(regParts[1].name, Registry.REG_SZ, regParts[1].value, error => {
if (error) {
//eslint-disable-next-line no-console
console.error(error.message);
}
hyperKey.set(regParts[2].name, Registry.REG_SZ, regParts[2].value, err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
commandKey.set(regParts[0].name, Registry.REG_SZ, regParts[0].value, err => {
if (err) {
console.error(err.message);
commandKey.set(regParts[0].name, Registry.REG_SZ, regParts[0].value, err_ => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
callback();
});
@ -27,24 +30,30 @@ function addValues(hyperKey, commandKey, callback) {
});
}
exports.add = function (callback) {
exports.add = callback => {
const hyperKey = new Registry({hive: 'HKCU', key: regKey});
const commandKey = new Registry({hive: 'HKCU', key: `${regKey}\\${regParts[0].key}`});
const commandKey = new Registry({
hive: 'HKCU',
key: `${regKey}\\${regParts[0].key}`
});
hyperKey.keyExists((err, exists) => {
if (err) {
console.error(err.message);
hyperKey.keyExists((error, exists) => {
if (error) {
//eslint-disable-next-line no-console
console.error(error.message);
}
if (exists) {
commandKey.keyExists((err, exists) => {
if (err) {
console.error(err.message);
commandKey.keyExists((err_, exists_) => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
if (exists) {
if (exists_) {
addValues(hyperKey, commandKey, callback);
} else {
commandKey.create(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
addValues(hyperKey, commandKey, callback);
@ -54,11 +63,13 @@ exports.add = function (callback) {
} else {
hyperKey.create(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
commandKey.create(err => {
if (err) {
console.error(err.message);
commandKey.create(err_ => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
addValues(hyperKey, commandKey, callback);
});
@ -67,9 +78,10 @@ exports.add = function (callback) {
});
};
exports.remove = function (callback) {
exports.remove = callback => {
new Registry({hive: 'HKCU', key: regKey}).destroy(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
callback();

27
app/ui/contextmenu.js Normal file
View file

@ -0,0 +1,27 @@
const editMenu = require('../menus/menus/edit');
const shellMenu = require('../menus/menus/shell');
const {execCommand} = require('../commands');
const {getDecoratedKeymaps} = require('../plugins');
const separator = {type: 'separator'};
const getCommandKeys = keymaps =>
Object.keys(keymaps).reduce((commandKeys, command) => {
return Object.assign(commandKeys, {
[command]: keymaps[command][0]
});
}, {});
// only display cut/copy when there's a cursor selection
const filterCutCopy = (selection, menuItem) => {
if (/^cut$|^copy$/.test(menuItem.role) && !selection) {
return;
}
return menuItem;
};
module.exports = (createWindow, selection) => {
const commandKeys = getCommandKeys(getDecoratedKeymaps());
const _shell = shellMenu(commandKeys, execCommand).submenu;
const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection));
return _edit.concat(separator, _shell).filter(menuItem => !menuItem.hasOwnProperty('enabled') || menuItem.enabled);
};

View file

@ -4,33 +4,43 @@ const {parse: parseUrl} = require('url');
const uuid = require('uuid');
const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev');
const AutoUpdater = require('../auto-updater');
const updater = require('../updater');
const toElectronBackgroundColor = require('../utils/to-electron-background-color');
const {icon, cfgDir} = require('../config/paths');
const createRPC = require('../rpc');
const notify = require('../notify');
const fetchNotifications = require('../notifications');
const Session = require('../session');
const contextMenuTemplate = require('./contextmenu');
const {execCommand} = require('../commands');
module.exports = class Window {
constructor(options, cfg, fn) {
const opts = Object.assign({
constructor(options_, cfg, fn) {
const winOpts = Object.assign(
{
minWidth: 370,
minHeight: 190,
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
titleBarStyle: 'hidden-inset',
title: 'Hyper.app',
// we want to go frameless on windows and linux
// we want to go frameless on Windows and Linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon,
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
acceptFirstMouse: true
}, options);
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(opts));
},
options_
);
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
const rpc = createRPC(window);
const sessions = new Map();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
@ -39,21 +49,19 @@ module.exports = class Window {
window.webContents.send('config change');
// notify user that shell changes require new sessions
if (cfg_.shell !== cfg.shell ||
JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
notify(
'Shell configuration changed!',
'Open a new tab or window to start using the new shell'
);
if (cfg_.shell !== cfg.shell || JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
notify('Shell configuration changed!', 'Open a new tab or window to start using the new shell');
}
// update background color if necessary
updateBackgroundColor();
cfg = cfg_;
});
rpc.on('init', () => {
window.setBackgroundColor(toElectronBackgroundColor(cfg.backgroundColor || '#000'));
window.show();
updateBackgroundColor();
// If no callback is passed to createWindow,
// a new session will be created by default.
@ -66,37 +74,41 @@ module.exports = class Window {
// and createWindow deifinition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(window);
delete (app.windowCallback);
delete app.windowCallback;
fetchNotifications(window);
// auto updates
if (!isDev && process.platform !== 'linux') {
AutoUpdater(window);
if (!isDev) {
updater(window);
} else {
//eslint-disable-next-line no-console
console.log('ignoring auto updates during dev');
}
});
rpc.on('new', options => {
const opts = Object.assign({
const sessionOpts = Object.assign(
{
rows: 40,
cols: 100,
cwd: process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : cfgDir,
splitDirection: undefined,
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
}, options);
},
options
);
const initSession = (opts, fn) => {
fn(uuid.v4(), new Session(opts));
const initSession = (opts, fn_) => {
fn_(uuid.v4(), new Session(opts));
};
initSession(opts, (uid, session) => {
initSession(sessionOpts, (uid, session) => {
sessions.set(uid, session);
rpc.emit('session add', {
rows: opts.rows,
cols: opts.cols,
rows: sessionOpts.rows,
cols: sessionOpts.cols,
uid,
splitDirection: opts.splitDirection,
splitDirection: sessionOpts.splitDirection,
shell: session.shell,
pid: session.pty.pid
});
@ -116,6 +128,7 @@ module.exports = class Window {
if (session) {
session.exit();
} else {
//eslint-disable-next-line no-console
console.log('session not found by', uid);
}
});
@ -136,9 +149,9 @@ module.exports = class Window {
const session = sessions.get(uid);
if (escaped) {
const escapedData = session.shell.endsWith('cmd.exe') ?
`"${data}"` : // This is how cmd.exe does it
`'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
const escapedData = session.shell.endsWith('cmd.exe')
? `"${data}"` // This is how cmd.exe does it
: `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
session.write(escapedData);
} else {
@ -148,6 +161,11 @@ module.exports = class Window {
rpc.on('open external', ({url}) => {
shell.openExternal(url);
});
rpc.on('open context menu', selection => {
const {createWindow} = app;
const {buildFromTemplate} = Menu;
buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup(window);
});
rpc.on('open hamburger menu', ({x, y}) => {
Menu.getApplicationMenu().popup(Math.ceil(x), Math.ceil(y));
});
@ -163,6 +181,10 @@ module.exports = class Window {
rpc.on('close', () => {
window.close();
});
rpc.on('command', command => {
const focusedWindow = BrowserWindow.getFocusedWindow();
execCommand(command, focusedWindow);
});
const deleteSessions = () => {
sessions.forEach((session, key) => {
session.removeAllListeners();
@ -195,6 +217,15 @@ module.exports = class Window {
}
});
// xterm makes link clickable
window.webContents.on('new-window', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
shell.openExternal(url);
}
});
// expose internals to extension authors
window.rpc = rpc;
window.sessions = sessions;
@ -210,6 +241,7 @@ module.exports = class Window {
if (!err) {
load();
window.webContents.send('plugins change');
updateBackgroundColor();
}
});

102
app/updater.js Normal file
View file

@ -0,0 +1,102 @@
// Packages
const electron = require('electron');
const {app} = electron;
const ms = require('ms');
const retry = require('async-retry');
// Utilities
// eslint-disable-next-line no-unused-vars
const notify = require('./notify');
const {version} = require('./package');
const {getDecoratedConfig} = require('./plugins');
const {platform} = process;
const isLinux = platform === 'linux';
const autoUpdater = isLinux ? require('./auto-updater-linux') : electron.autoUpdater;
let isInit = false;
// Default to the "stable" update channel
let canaryUpdates = false;
const buildFeedUrl = (canary, currentVersion) => {
const updatePrefix = canary ? 'releases-canary' : 'releases';
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
};
const isCanary = updateChannel => updateChannel === 'canary';
async function init() {
autoUpdater.on('error', (err, msg) => {
//eslint-disable-next-line no-console
console.error('Error fetching updates', msg + ' (' + err.stack + ')');
});
const config = await retry(async () => {
const content = await getDecoratedConfig();
if (!content) {
throw new Error('No config content loaded');
}
return content;
});
// If defined in the config, switch to the "canary" channel
if (config.updateChannel && isCanary(config.updateChannel)) {
canaryUpdates = true;
}
const feedURL = buildFeedUrl(canaryUpdates, version);
autoUpdater.setFeedURL(feedURL);
setTimeout(() => {
autoUpdater.checkForUpdates();
}, ms('10s'));
setInterval(() => {
autoUpdater.checkForUpdates();
}, ms('30m'));
isInit = true;
}
module.exports = win => {
if (!isInit) {
init();
}
const {rpc} = win;
const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => {
const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
};
const eventName = isLinux ? 'update-available' : 'update-downloaded';
autoUpdater.on(eventName, onupdate);
rpc.once('quit and install', () => {
autoUpdater.quitAndInstall();
});
app.config.subscribe(() => {
const {updateChannel} = app.plugins.getDecoratedConfig();
const newUpdateIsCanary = isCanary(updateChannel);
if (newUpdateIsCanary !== canaryUpdates) {
const feedURL = buildFeedUrl(newUpdateIsCanary, version);
autoUpdater.setFeedURL(feedURL);
autoUpdater.checkForUpdates();
canaryUpdates = newUpdateIsCanary;
}
});
win.on('close', () => {
autoUpdater.removeListener(eventName, onupdate);
});
};

91
app/utils/cli-install.js Normal file
View file

@ -0,0 +1,91 @@
const pify = require('pify');
const fs = require('fs');
const path = require('path');
const Registry = require('winreg');
const {cliScriptPath} = require('../config/paths');
const lstat = pify(fs.lstat);
const readlink = pify(fs.readlink);
const unlink = pify(fs.unlink);
const symlink = pify(fs.symlink);
const target = '/usr/local/bin/hyper';
const source = cliScriptPath;
const checkInstall = () => {
return lstat(target)
.then(stat => stat.isSymbolicLink())
.then(() => readlink(target))
.then(link => link === source)
.catch(err => {
if (err.code === 'ENOENT') {
return false;
}
throw err;
});
};
const createSymlink = () => {
return unlink(target)
.catch(err => {
if (err.code === 'ENOENT') {
return;
}
throw err;
})
.then(() => symlink(source, target));
};
exports.addSymlink = () => {
return checkInstall().then(isInstalled => {
if (isInstalled) {
return Promise.resolve();
}
return createSymlink();
});
};
exports.addBinToUserPath = () => {
// Can't use pify because of param order of Registry.values callback
return new Promise((resolve, reject) => {
const envKey = new Registry({hive: 'HKCU', key: '\\Environment'});
envKey.values((err, items) => {
if (err) {
reject(err);
return;
}
// C:\Users\<user>\AppData\Local\hyper\app-<version>\resources\bin
const binPath = path.dirname(cliScriptPath);
// C:\Users\<user>\AppData\Local\hyper
const basePath = path.resolve(binPath, '../../..');
const pathItem = items.find(item => item.name.toUpperCase() === 'PATH');
let newPathValue = binPath;
const pathItemName = pathItem ? pathItem.name : 'PATH';
if (pathItem) {
const pathParts = pathItem.value.split(';');
const existingPath = pathParts.find(pathPart => pathPart === binPath);
if (existingPath) {
resolve();
return;
}
// Because version is in path we need to remove old path if present and add current path
newPathValue = pathParts
.filter(pathPart => !pathPart.startsWith(basePath))
.concat([binPath])
.join(';');
}
envKey.set(pathItemName, Registry.REG_SZ, newPathValue, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
};

View file

@ -19,13 +19,14 @@ const colorList = [
'grayscale'
];
export default function getColorList(colors) {
// For backwards compatibility, return early if it's already an array
if (Array.isArray(colors)) {
exports.getColorMap = colors => {
if (!Array.isArray(colors)) {
return colors;
}
return colorList.map(colorName => {
return colors[colorName];
});
return colors.reduce((result, color, index) => {
if (index < colorList.length) {
result[colorList[index]] = color;
}
return result;
}, {});
};

View file

@ -1,5 +0,0 @@
const normalize = require('./normalize');
module.exports = (keys, commands) => {
return commands[normalize(keys)];
};

View file

@ -1,6 +0,0 @@
const {getKeymaps} = require('../../config');
const findCommandByKeys = require('./find-command-by-keys');
module.exports = keys => {
return findCommandByKeys(keys, getKeymaps().keys);
};

View file

@ -1,13 +0,0 @@
// This function receives a keymap in any key order and returns
// the same keymap alphatetically sorted by the clients locale.
// eg.: cmd+alt+o -> alt+cmd+o
// We do this in order to normalize what the user defined to what we
// internally parse. By doing this, you can set your keymaps in any given order
// eg.: alt+cmd+o, cmd+alt+o, o+alt+cmd, etc. #2195
module.exports = keybinding => {
function sortAlphabetically(a, b) {
return a.localeCompare(b);
}
return keybinding.toLowerCase().split('+').sort(sortAlphabetically).join('+');
};

41
app/utils/map-keys.js Normal file
View file

@ -0,0 +1,41 @@
const generatePrefixedCommand = (command, shortcuts) => {
const result = {};
const baseCmd = command.replace(/:prefix$/, '');
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const index = i === 9 ? 'last' : i;
const prefixedShortcuts = shortcuts.map(shortcut => `${shortcut}+${i}`);
result[`${baseCmd}:${index}`] = prefixedShortcuts;
}
return result;
};
module.exports = config => {
return Object.keys(config).reduce((keymap, command) => {
if (!command) {
return;
}
// We can have different keys for a same command.
const shortcuts = Array.isArray(config[command]) ? config[command] : [config[command]];
const fixedShortcuts = [];
shortcuts.forEach(shortcut => {
let newShortcut = shortcut;
if (newShortcut.indexOf('cmd') !== -1) {
// Mousetrap use `command` and not `cmd`
//eslint-disable-next-line no-console
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
newShortcut = newShortcut.replace('cmd', 'command');
}
fixedShortcuts.push(newShortcut);
});
if (command.endsWith(':prefix')) {
return Object.assign(keymap, generatePrefixedCommand(command, fixedShortcuts));
}
keymap[command] = fixedShortcuts;
return keymap;
}, {});
};

View file

@ -13,5 +13,12 @@ module.exports = bgColor => {
// http://stackoverflow.com/a/11019879/1202488
const alphaHex = Math.round(color.alpha() * 255).toString(16);
return '#' + alphaHex + color.hex().toString().substr(1);
return (
'#' +
alphaHex +
color
.hex()
.toString()
.substr(1)
);
};

14
app/utils/window-utils.js Normal file
View file

@ -0,0 +1,14 @@
const electron = require('electron');
function positionIsValid(position) {
const displays = electron.screen.getAllDisplays();
const [x, y] = position;
return displays.some(({workArea}) => {
return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height;
});
}
module.exports = {
positionIsValid
};

518
app/yarn-error.log Normal file
View file

@ -0,0 +1,518 @@
Arguments:
/usr/local/bin/node /usr/local/Cellar/yarn/1.5.1_1/libexec/bin/yarn.js install --production
PATH:
/Users/leo/projects/hyper/node_modules/.bin:/Users/leo/.config/yarn/link/node_modules/.bin:/Users/leo/projects/hyper/node_modules/.bin:/Users/leo/.config/yarn/link/node_modules/.bin:/usr/local/libexec/lib/node_modules/npm/bin/node-gyp-bin:/usr/local/lib/node_modules/npm/bin/node-gyp-bin:/usr/local/bin/node_modules/npm/bin/node-gyp-bin:/usr/local/opt/openvpn/sbin:/Users/leo/google-cloud-sdk/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/opt/X11/bin
Yarn version:
1.5.1
Node version:
9.10.1
Platform:
darwin x64
npm manifest:
{
"name": "hyper",
"productName": "Hyper",
"description": "A terminal built on web technologies",
"version": "2.0.0-canary.17",
"license": "MIT",
"author": {
"name": "ZEIT, Inc.",
"email": "team@zeit.co"
},
"repository": "zeit/hyper",
"dependencies": {
"async-retry": "1.1.4",
"color": "2.0.1",
"convert-css-color-name-to-hex": "0.1.1",
"default-shell": "1.0.1",
"electron-config": "1.0.0",
"electron-is-dev": "0.3.0",
"electron-squirrel-startup": "1.0.0",
"file-uri-to-path": "1.0.0",
"git-describe": "4.0.2",
"lodash": "4.17.5",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-fetch": "1.7.3",
"node-pty": "0.7.4",
"parse-url": "3.0.2",
"queue": "4.4.2",
"semver": "5.5.0",
"shell-env": "0.3.0",
"uuid": "3.2.1",
"winreg": "1.2.4"
}
}
yarn manifest:
No manifest
Lockfile:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
async-retry@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.1.4.tgz#e0edb546600f19bf90f892e9494faa9e19baf190"
dependencies:
retry "0.10.1"
color-convert@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
dependencies:
color-name "^1.1.1"
color-name@^1.0.0, color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
color-string@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839"
dependencies:
color-convert "^1.9.1"
color-string "^1.5.2"
conf@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/conf/-/conf-1.2.0.tgz#149af7408f0af6abd62c3e24cff747e41a0bc54f"
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
make-dir "^1.0.0"
pkg-up "^2.0.0"
write-file-atomic "^2.3.0"
convert-css-color-name-to-hex@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/convert-css-color-name-to-hex/-/convert-css-color-name-to-hex-0.1.1.tgz#38ac4d27ca470593fd663b18a072a308926a35a2"
dependencies:
css-color-names "0.0.3"
is-css-color-name "^0.1.1"
cross-spawn@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
dependencies:
lru-cache "^4.0.1"
which "^1.2.9"
css-color-names@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.2.tgz#fba18e8cff86579572d749c146c47ee83f0ea955"
css-color-names@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.3.tgz#de0cef16f4d8aa8222a320d5b6d7e9bbada7b9f6"
debug@^2.2.0:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "2.0.0"
default-shell@1.0.1, default-shell@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc"
dot-prop@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
dependencies:
is-obj "^1.0.0"
electron-config@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-config/-/electron-config-1.0.0.tgz#069d044cc794f04784ae72f12916725d3c8c39af"
dependencies:
conf "^1.0.0"
electron-is-dev@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
electron-squirrel-startup@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz#19b4e55933fa0ef8f556784b9c660f772546a0b8"
dependencies:
debug "^2.2.0"
encoding@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
dependencies:
iconv-lite "~0.4.13"
env-paths@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
execa@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36"
dependencies:
cross-spawn "^4.0.0"
get-stream "^2.2.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
dependencies:
locate-path "^2.0.0"
get-stream@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
dependencies:
object-assign "^4.0.1"
pinkie-promise "^2.0.0"
git-describe@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/git-describe/-/git-describe-4.0.2.tgz#9ecc5e6df4658450c89ea35d10183c9f5f40dc73"
dependencies:
lodash "^4.16.6"
optionalDependencies:
semver "^5.3.0"
graceful-fs@^4.1.11:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
iconv-lite@~0.4.13:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
inherits@~2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
is-arrayish@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd"
is-css-color-name@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/is-css-color-name/-/is-css-color-name-0.1.3.tgz#ea3b51bc901d8a243d32c9b7873d0680dbbef7f1"
dependencies:
css-color-names "0.0.2"
is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
is-ssh@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.0.tgz#ebea1169a2614da392a63740366c3ce049d8dff6"
dependencies:
protocols "^1.1.0"
is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
dependencies:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash@4.17.5:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.16.6:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lru-cache@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
make-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
dependencies:
pify "^2.3.0"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
mkdirp@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
nan@^2.6.2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
node-fetch@1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-pty@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.4.tgz#07146b2b40b76e432e57ce6750bda40f0da5c99f"
dependencies:
nan "^2.6.2"
normalize-url@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
dependencies:
object-assign "^4.0.1"
prepend-http "^1.0.0"
query-string "^4.1.0"
sort-keys "^1.0.0"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
dependencies:
path-key "^2.0.0"
object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
p-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
dependencies:
p-limit "^1.1.0"
parse-path@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-3.0.2.tgz#4686980f0b262ee2dbb9a64eef739c91edc85245"
dependencies:
is-ssh "^1.3.0"
protocols "^1.4.0"
parse-url@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-3.0.2.tgz#602787a7063a795d72b8673197505e72f60610be"
dependencies:
is-ssh "^1.3.0"
normalize-url "^1.9.1"
parse-path "^3.0.1"
protocols "^1.4.0"
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
path-key@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
dependencies:
pinkie "^2.0.0"
pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies:
find-up "^2.1.0"
prepend-http@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
protocols@^1.1.0, protocols@^1.4.0:
version "1.4.6"
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.6.tgz#f8bb263ea1b5fd7a7604d26b8be39bd77678bf8a"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
dependencies:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
queue@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/queue/-/queue-4.4.2.tgz#5a9733d9a8b8bd1b36e934bc9c55ab89b28e29c7"
dependencies:
inherits "~2.0.0"
retry@0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
shell-env@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-0.3.0.tgz#2250339022989165bda4eb7bf383afeaaa92dc34"
dependencies:
default-shell "^1.0.0"
execa "^0.5.0"
strip-ansi "^3.0.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
dependencies:
is-arrayish "^0.3.1"
sort-keys@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
dependencies:
is-plain-obj "^1.0.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
ansi-regex "^2.0.0"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
uuid@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
isexe "^2.0.0"
winreg@1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
write-file-atomic@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
dependencies:
graceful-fs "^4.1.11"
imurmurhash "^0.1.4"
signal-exit "^3.0.2"
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
Trace:
Error: /Users/leo/projects/hyper/app/node_modules/node-pty: Command failed.
Exit code: 1
Command: sh
Arguments: -c node scripts/install.js
Directory: /Users/leo/projects/hyper/app/node_modules/node-pty
Output:
module.js:545
throw err;
^
Error: Cannot find module '/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js'
at Function.Module._resolveFilename (module.js:543:15)
at Function.Module._load (module.js:470:25)
at Function.Module.runMain (module.js:690:10)
at startup (bootstrap_node.js:194:16)
at bootstrap_node.js:666:3
at ProcessTermError.MessageError (/usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:186:110)
at new ProcessTermError (/usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:226:113)
at ChildProcess.<anonymous> (/usr/local/Cellar/yarn/1.5.1_1/libexec/lib/cli.js:30281:17)
at ChildProcess.emit (events.js:180:13)
at maybeClose (internal/child_process.js:936:16)
at Socket.stream.socket.on (internal/child_process.js:353:11)
at Socket.emit (events.js:180:13)
at Pipe._handle.close [as _onclose] (net.js:541:12)

View file

@ -6,15 +6,15 @@ ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
async-retry@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.1.3.tgz#1be664783337a0614999d543009a3b6e0de3609d"
async-retry@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.1.4.tgz#e0edb546600f19bf90f892e9494faa9e19baf190"
dependencies:
retry "0.10.1"
color-convert@^1.8.2:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
color-convert@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
dependencies:
color-name "^1.1.1"
@ -22,19 +22,19 @@ color-name@^1.0.0, color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
color-string@^1.4.0:
color-string@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/color/-/color-2.0.0.tgz#e0c9972d1e969857004b101eaa55ceab5961d67d"
color@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839"
dependencies:
color-convert "^1.8.2"
color-string "^1.4.0"
color-convert "^1.9.1"
color-string "^1.5.2"
conf@^1.0.0:
version "1.2.0"
@ -177,6 +177,16 @@ is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
is-ssh@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.0.tgz#ebea1169a2614da392a63740366c3ce049d8dff6"
dependencies:
protocols "^1.1.0"
is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@ -192,7 +202,11 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash@4.17.4, lodash@^4.16.6:
lodash@4.17.5:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.16.6:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@ -223,30 +237,43 @@ ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
nan@^2.6.2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
node-fetch@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7"
node-fetch@1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-pty@0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.0.tgz#993038692d3a2921811a152658fc7dc211323ac2"
node-pty@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.4.tgz#07146b2b40b76e432e57ce6750bda40f0da5c99f"
dependencies:
nan "^2.6.2"
normalize-url@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
dependencies:
object-assign "^4.0.1"
prepend-http "^1.0.0"
query-string "^4.1.0"
sort-keys "^1.0.0"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
dependencies:
path-key "^2.0.0"
object-assign@^4.0.1:
object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -264,6 +291,22 @@ p-locate@^2.0.0:
dependencies:
p-limit "^1.1.0"
parse-path@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-3.0.2.tgz#4686980f0b262ee2dbb9a64eef739c91edc85245"
dependencies:
is-ssh "^1.3.0"
protocols "^1.4.0"
parse-url@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-3.0.2.tgz#602787a7063a795d72b8673197505e72f60610be"
dependencies:
is-ssh "^1.3.0"
normalize-url "^1.9.1"
parse-path "^3.0.1"
protocols "^1.4.0"
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -292,13 +335,28 @@ pkg-up@^2.0.0:
dependencies:
find-up "^2.1.0"
prepend-http@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
protocols@^1.1.0, protocols@^1.4.0:
version "1.4.6"
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.6.tgz#f8bb263ea1b5fd7a7604d26b8be39bd77678bf8a"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
queue@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/queue/-/queue-4.4.0.tgz#bf9139e62f440a00de4a7d5e1907a91282e4560d"
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
dependencies:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
queue@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/queue/-/queue-4.4.2.tgz#5a9733d9a8b8bd1b36e934bc9c55ab89b28e29c7"
dependencies:
inherits "~2.0.0"
@ -306,7 +364,11 @@ retry@0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
semver@5.4.1, semver@^5.3.0:
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
@ -328,6 +390,16 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
sort-keys@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
dependencies:
is-plain-obj "^1.0.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -338,9 +410,9 @@ strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
uuid@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
uuid@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
which@^1.2.9:
version "1.3.0"

View file

@ -27,5 +27,6 @@ test_script:
- yarn run test
on_success:
- IF %APPVEYOR_REPO_BRANCH%==canary cp build\canary.ico build\icon.ico
- yarn run dist
- ps: ls .\dist\win\*.exe | % { Push-AppveyorArtifact $_.FullName }
- ps: Get-ChildItem .\dist\squirrel-windows\*.exe | % { Push-AppveyorArtifact $_.FullName }

File diff suppressed because one or more lines are too long

BIN
build/canary.icns Normal file

Binary file not shown.

BIN
build/canary.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View file

@ -0,0 +1,4 @@
#!/bin/bash
# Link to the CLI bootstrap
ln -sf '/opt/${productFilename}/resources/bin/${executable}' '/usr/local/bin/${executable}'

34
build/linux/hyper Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/linux/bin/code.sh
# If root, ensure that --user-data-dir is specified
if [ "$(id -u)" = "0" ]; then
for i in $@
do
if [[ $i == --user-data-dir=* ]]; then
DATA_DIR_SET=1
fi
done
if [ -z $DATA_DIR_SET ]; then
echo "It is recommended to start hyper as a normal user. To run as root, you must specify an alternate user data directory with the --user-data-dir argument." 1>&2
exit 1
fi
fi
if [ ! -L $0 ]; then
# if path is not a symlink, find relatively
HYPER_PATH="$(dirname $0)/../.."
else
if which readlink >/dev/null; then
# if readlink exists, follow the symlink and find relatively
HYPER_PATH="$(dirname $(readlink -f $0))/../.."
else
# else use the standard install location
HYPER_PATH="/opt/Hyper"
fi
fi
ELECTRON="$HYPER_PATH/hyper"
CLI="$HYPER_PATH/resources/bin/cli.js"
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?

9
build/mac/hyper Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/darwin/bin/code.sh
function realpath() { /usr/bin/python -c "import os,sys; print os.path.realpath(sys.argv[1])" "$0"; }
CONTENTS="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
ELECTRON="$CONTENTS/MacOS/Hyper"
CLI="$CONTENTS/Resources/bin/cli.js"
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?

25
build/win/hyper Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/win/bin/code.sh
NAME="Hyper"
HYPER_PATH="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
ELECTRON="$HYPER_PATH/$NAME.exe"
if grep -q Microsoft /proc/version; then
echo "Warning! Due to WSL limitations, you can use CLI commands here. Please use Hyper CLI on cmd, PowerShell or GitBash/CygWin."
echo "Please see: https://github.com/Microsoft/WSL/issues/1494"
echo ""
# If running under WSL don't pass cli.js to Electron as environment vars
# cannot be transferred from WSL to Windows
# See: https://github.com/Microsoft/BashOnWindows/issues/1363
# https://github.com/Microsoft/BashOnWindows/issues/1494
"$ELECTRON" "$@"
exit $?
fi
if [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then
CLI=$(cygpath -m "$HYPER_PATH/resources/bin/cli.js")
else
CLI="$HYPER_PATH/resources/bin/cli.js"
fi
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?

5
build/win/hyper.cmd Executable file
View file

@ -0,0 +1,5 @@
@echo off
setlocal
set ELECTRON_RUN_AS_NODE=1
call "%~dp0..\..\Hyper.exe" "%~dp0..\..\resources\bin\cli.js" %*
endlocal

View file

@ -1,31 +0,0 @@
machine:
xcode:
version: 8.2
pre:
- mkdir ~/.yarn-cache
dependencies:
pre:
- npm install -g yarn
- yarn config set cache-folder ~/.yarn-cache
cache_directories:
- ~/.yarn-cache
override:
- yarn
test:
override:
- yarn test
deployment:
artifacts:
branch: /^(?!master$).*$/
owner: zeit
commands:
- yarn run dist -- -p 'never'
- cp dist/*.zip $CIRCLE_ARTIFACTS
release:
tag: /.*/
owner: zeit
commands:
- yarn run dist

127
cli/api.js Normal file
View file

@ -0,0 +1,127 @@
const fs = require('fs');
const os = require('os');
const got = require('got');
const registryUrl = require('registry-url')();
const pify = require('pify');
const recast = require('recast');
const fileName = `${os.homedir()}/.hyper.js`;
/**
* We need to make sure the file reading and parsing is lazy so that failure to
* statically analyze the hyper configuration isn't fatal for all kinds of
* subcommands. We can use memoization to make reading and parsing lazy.
*/
function memoize(fn) {
let hasResult = false;
let result;
return (...args) => {
if (!hasResult) {
result = fn(...args);
hasResult = true;
}
return result;
};
}
const getFileContents = memoize(() => {
try {
return fs.readFileSync(fileName, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
// ENOENT === !exists()
throw err;
}
}
return null;
});
const getParsedFile = memoize(() => recast.parse(getFileContents()));
const getProperties = memoize(() => getParsedFile().program.body[0].expression.right.properties);
const getPlugins = memoize(() => getProperties().find(property => property.key.name === 'plugins').value.elements);
const getLocalPlugins = memoize(
() => getProperties().find(property => property.key.name === 'localPlugins').value.elements
);
function exists() {
return getFileContents() !== undefined;
}
function isInstalled(plugin, locally) {
const array = locally ? getLocalPlugins() : getPlugins();
if (array && Array.isArray(array)) {
return array.find(entry => entry.value === plugin) !== undefined;
}
return false;
}
function save() {
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
}
function existsOnNpm(plugin) {
const name = plugin.split('#')[0].split('@')[0];
return got.get(registryUrl + name.toLowerCase(), {timeout: 10000, json: true}).then(res => {
if (!res.body.versions) {
return Promise.reject(res);
}
});
}
function install(plugin, locally) {
const array = locally ? getLocalPlugins() : getPlugins();
return new Promise((resolve, reject) => {
existsOnNpm(plugin)
.then(() => {
if (isInstalled(plugin, locally)) {
return reject(`${plugin} is already installed`);
}
array.push(recast.types.builders.literal(plugin));
save()
.then(resolve)
.catch(err => reject(err));
})
.catch(err => {
const {statusCode} = err;
if (statusCode && (statusCode === 404 || statusCode === 200)) {
return reject(`${plugin} not found on npm`);
}
return reject(`${err.message}\nPlugin check failed. Check your internet connection or retry later.`);
});
});
}
function uninstall(plugin) {
return new Promise((resolve, reject) => {
if (!isInstalled(plugin)) {
return reject(`${plugin} is not installed`);
}
const index = getPlugins().findIndex(entry => entry.value === plugin);
getPlugins().splice(index, 1);
save()
.then(resolve)
.catch(err => reject(err));
});
}
function list() {
if (Array.isArray(getPlugins())) {
return getPlugins()
.map(plugin => plugin.value)
.join('\n');
}
return false;
}
module.exports.configPath = fileName;
module.exports.exists = exists;
module.exports.existsOnNpm = existsOnNpm;
module.exports.isInstalled = isInstalled;
module.exports.install = install;
module.exports.uninstall = uninstall;
module.exports.list = list;

213
cli/index.js Normal file
View file

@ -0,0 +1,213 @@
// This is a CLI tool, using console is OK
/* eslint no-console: 0 */
const {spawn, exec} = require('child_process');
const {isAbsolute, resolve} = require('path');
const {existsSync} = require('fs');
const pify = require('pify');
const args = require('args');
const chalk = require('chalk');
const opn = require('opn');
const columnify = require('columnify');
const got = require('got');
const ora = require('ora');
const api = require('./api');
const PLUGIN_PREFIX = 'hyper-';
let commandPromise;
const assertPluginName = pluginName => {
if (!pluginName) {
console.error(chalk.red('Plugin name is required'));
process.exit(1);
}
};
const checkConfig = () => {
if (api.exists()) {
return true;
}
let msg = chalk.red(`Error! Config file not found: ${api.configPath}\n`);
msg += 'Please launch Hyper and retry.';
console.error(msg);
process.exit(1);
};
args.command(['i', 'install'], 'Install a plugin', (name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.install(pluginName)
.then(() => console.log(chalk.green(`${pluginName} installed successfully!`)))
.catch(err => console.error(chalk.red(err)));
});
args.command(['u', 'uninstall', 'rm', 'remove'], 'Uninstall a plugin', (name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.uninstall(pluginName)
.then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`)))
.catch(err => console.log(chalk.red(err)));
});
args.command(['ls', 'list'], 'List installed plugins', () => {
checkConfig();
let plugins = api.list();
if (plugins) {
console.log(plugins);
} else {
console.log(chalk.red(`No plugins installed yet.`));
}
process.exit(0);
});
const lsRemote = pattern => {
// note that no errors are catched by this function
const URL = `https://api.npms.io/v2/search?q=${(pattern && `${pattern}+`) || ''}keywords:hyper-plugin,hyper-theme`;
return got(URL)
.then(response => JSON.parse(response.body).results)
.then(entries => entries.map(entry => entry.package))
.then(entries => entries.filter(entry => entry.name.indexOf(PLUGIN_PREFIX) === 0))
.then(entries =>
entries.map(({name, description}) => {
return {name, description};
})
)
.then(entries =>
entries.map(entry => {
entry.name = chalk.green(entry.name);
return entry;
})
);
};
args.command(['s', 'search'], 'Search for plugins on npm', (name, args_) => {
const spinner = ora('Searching').start();
const query = args_[0] ? args_[0].toLowerCase() : '';
commandPromise = lsRemote(query)
.then(entries => {
if (entries.length === 0) {
spinner.fail();
console.error(chalk.red(`Your search '${query}' did not match any plugins`));
console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`);
process.exit(1);
} else {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
}
})
.catch(err => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
});
args.command(['lsr', 'list-remote', 'ls-remote'], 'List plugins available on npm', () => {
const spinner = ora('Searching').start();
commandPromise = lsRemote()
.then(entries => {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
})
.catch(err => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
});
args.command(['d', 'docs', 'h', 'home'], 'Open the npm page of a plugin', (name, args_) => {
const pluginName = args_[0];
assertPluginName(pluginName);
opn(`http://ghub.io/${pluginName}`, {wait: false});
process.exit(0);
});
args.command(['<default>'], 'Launch Hyper');
args.option(['v', 'verbose'], 'Verbose mode', false);
const main = argv => {
const flags = args.parse(argv, {
name: 'hyper',
version: false,
mri: {
boolean: ['v', 'verbose']
}
});
if (commandPromise) {
return commandPromise;
}
const env = Object.assign({}, process.env, {
// this will signal Hyper that it was spawned from this module
HYPER_CLI: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
});
delete env['ELECTRON_RUN_AS_NODE'];
if (flags.verbose) {
env['ELECTRON_ENABLE_LOGGING'] = '1';
}
const options = {
detached: true,
env
};
const args_ = args.sub.map(arg => {
const cwd = isAbsolute(arg) ? arg : resolve(process.cwd(), arg);
if (!existsSync(cwd)) {
console.error(chalk.red(`Error! Directory or file does not exist: ${cwd}`));
process.exit(1);
}
return cwd;
});
if (!flags.verbose) {
options['stdio'] = 'ignore';
if (process.platform === 'darwin') {
//Use `open` to prevent multiple Hyper process
const cmd = `open -b co.zeit.hyper ${args_}`;
const opts = {
env
};
return pify(exec)(cmd, opts);
}
}
const child = spawn(process.execPath, args_, options);
if (flags.verbose) {
child.stdout.on('data', data => console.log(data.toString('utf8')));
child.stderr.on('data', data => console.error(data.toString('utf8')));
}
if (flags.verbose) {
return new Promise(c => child.once('exit', () => c(null)));
}
child.unref();
return Promise.resolve();
};
function eventuallyExit(code) {
setTimeout(() => process.exit(code), 100);
}
main(process.argv)
.then(() => eventuallyExit(0))
.catch(err => {
console.error(err.stack ? err.stack : err);
eventuallyExit(1);
});

7
jsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"jsx": "react",
"target": "es6"
},
"exclude": ["node_modules", "**/node_modules/*", "bin/*", "renderer/*"]
}

View file

@ -8,8 +8,10 @@ export function loadConfig(config) {
}
export function reloadConfig(config) {
const now = Date.now();
return {
type: CONFIG_RELOAD,
config
config,
now
};
}

View file

@ -1,5 +1,11 @@
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE, UI_OPEN_HAMBURGER_MENU, UI_WINDOW_MINIMIZE, UI_WINDOW_CLOSE} from '../constants/ui';
import {
UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE,
UI_OPEN_HAMBURGER_MENU,
UI_WINDOW_MINIMIZE,
UI_WINDOW_CLOSE
} from '../constants/ui';
import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups';

View file

@ -1,7 +1,4 @@
import {
NOTIFICATION_MESSAGE,
NOTIFICATION_DISMISS
} from '../constants/notifications';
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
export function dismissNotification(id) {
return {

View file

@ -38,11 +38,11 @@ export function addSession({uid, shell, pid, cols, rows, splitDirection}) {
export function requestSession() {
return (dispatch, getState) => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
rpc.emit('new', {cols, rows, cwd});
}
});
@ -50,7 +50,7 @@ export function requestSession() {
}
export function addSessionData(uid, data) {
return function (dispatch, getState) {
return (dispatch, getState) => {
dispatch({
type: SESSION_ADD_DATA,
data,
@ -147,7 +147,7 @@ export function resizeSession(uid, cols, rows) {
}
export function sendSessionData(uid, data, escaped) {
return function (dispatch, getState) {
return (dispatch, getState) => {
dispatch({
type: SESSION_USER_DATA,
data,

View file

@ -13,10 +13,10 @@ import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
function requestSplit(direction) {
return () => (dispatch, getState) => {
const {ui} = getState();
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui} = getState();
rpc.emit('new', {
splitDirection: direction,
cwd: ui.cwd
@ -39,11 +39,11 @@ export function resizeTermGroup(uid, sizes) {
export function requestTermGroup() {
return (dispatch, getState) => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
dispatch({
type: TERM_GROUP_REQUEST,
effect: () => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
rpc.emit('new', {
isNewGroup: true,
cols,

View file

@ -5,11 +5,7 @@ import getRootGroups from '../selectors';
import findBySession from '../utils/term-groups';
import notify from '../utils/notify';
import rpc from '../rpc';
import {
requestSession,
sendSessionData,
setActiveSession
} from '../actions/sessions';
import {requestSession, sendSessionData, setActiveSession} from '../actions/sessions';
import {
UI_FONT_SIZE_SET,
UI_FONT_SIZE_INCR,
@ -23,13 +19,33 @@ import {
UI_MOVE_PREV_PANE,
UI_WINDOW_GEOMETRY_CHANGED,
UI_WINDOW_MOVE,
UI_OPEN_FILE
UI_OPEN_FILE,
UI_OPEN_SSH_URL,
UI_CONTEXTMENU_OPEN,
UI_COMMAND_EXEC
} from '../constants/ui';
import {setActiveGroup} from './term-groups';
import parseUrl from 'parse-url';
const {stat} = window.require('fs');
export function openContextMenu(uid, selection) {
return (dispatch, getState) => {
dispatch({
type: UI_CONTEXTMENU_OPEN,
uid,
effect() {
const state = getState();
const show = !state.ui.quickEdit;
if (show) {
rpc.emit('open context menu', selection);
}
}
});
};
}
export function increaseFontSize() {
return (dispatch, getState) => {
dispatch({
@ -74,9 +90,7 @@ export function setFontSmoothing() {
return dispatch => {
setTimeout(() => {
const devicePixelRatio = window.devicePixelRatio;
const fontSmoothing = devicePixelRatio < 2 ?
'subpixel-antialiased' :
'antialiased';
const fontSmoothing = devicePixelRatio < 2 ? 'subpixel-antialiased' : 'antialiased';
dispatch({
type: UI_FONT_SMOOTHING_SET,
@ -100,11 +114,7 @@ const findChildSessions = (termGroups, uid) => {
return [uid];
}
return group
.children
.reduce((total, childUid) => total.concat(
findChildSessions(termGroups, childUid)
), []);
return group.children.reduce((total, childUid) => total.concat(findChildSessions(termGroups, childUid)), []);
};
// Get the index of the next or previous group,
@ -126,6 +136,7 @@ function moveToNeighborPane(type) {
const {uid} = findBySession(termGroups, sessions.activeUid);
const childGroups = findChildSessions(termGroups.termGroups, termGroups.activeRootGroup);
if (childGroups.length === 1) {
//eslint-disable-next-line no-console
console.log('ignoring move for single group');
} else {
const index = getNeighborIndex(childGroups, uid, type);
@ -156,6 +167,7 @@ export function moveLeft() {
const index = groupUids.indexOf(uid);
const next = groupUids[index - 1] || last(groupUids);
if (!next || uid === next) {
//eslint-disable-next-line no-console
console.log('ignoring left move action');
} else {
dispatch(setActiveGroup(next));
@ -176,6 +188,7 @@ export function moveRight() {
const index = groupUids.indexOf(uid);
const next = groupUids[index + 1] || groupUids[0];
if (!next || uid === next) {
//eslint-disable-next-line no-console
console.log('ignoring right move action');
} else {
dispatch(setActiveGroup(next));
@ -187,6 +200,14 @@ export function moveRight() {
export function moveTo(i) {
return (dispatch, getState) => {
if (i === 'last') {
// Finding last tab index
const {termGroups} = getState().termGroups;
i =
Object.keys(termGroups)
.map(uid => termGroups[uid])
.filter(({parentUid}) => !parentUid).length - 1;
}
dispatch({
type: UI_MOVE_TO,
index: i,
@ -195,10 +216,12 @@ export function moveTo(i) {
const groupUids = getGroupUids(state);
const uid = state.termGroups.activeRootGroup;
if (uid === groupUids[i]) {
//eslint-disable-next-line no-console
console.log('ignoring same uid');
} else if (groupUids[i]) {
dispatch(setActiveGroup(groupUids[i]));
} else {
//eslint-disable-next-line no-console
console.log('ignoring inexistent index', i);
}
}
@ -235,6 +258,7 @@ export function openFile(path) {
effect() {
stat(path, (err, stats) => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Unable to open path', `"${path}" doesn't exist.`);
} else {
@ -256,3 +280,42 @@ export function openFile(path) {
});
};
}
export function openSSH(url) {
return dispatch => {
dispatch({
type: UI_OPEN_SSH_URL,
effect() {
let parsedUrl = parseUrl(url, true);
let command = parsedUrl.protocol + ' ' + (parsedUrl.user || '') + '@' + parsedUrl.resource;
if (parsedUrl.port) command += ' -p ' + parsedUrl.port;
command += '\n';
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
});
});
dispatch(requestSession());
}
});
};
}
export function execCommand(command, fn, e) {
return dispatch =>
dispatch({
type: UI_COMMAND_EXEC,
command,
effect() {
if (fn) {
fn(e);
} else {
rpc.emit('command', command);
}
}
});
}

View file

@ -1,7 +1,4 @@
import {
UPDATE_INSTALL,
UPDATE_AVAILABLE
} from '../constants/updater';
import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../constants/updater';
import rpc from '../rpc';
export function installUpdate() {
@ -13,10 +10,12 @@ export function installUpdate() {
};
}
export function updateAvailable(version, notes) {
export function updateAvailable(version, notes, releaseUrl, canInstall) {
return {
type: UPDATE_AVAILABLE,
version,
notes
notes,
releaseUrl,
canInstall
};
}

View file

@ -1,23 +1,46 @@
const commands = {};
import {remote} from 'electron';
class CommandRegistry {
register(cmds) {
if (cmds) {
for (const command in cmds) {
if (command) {
commands[command] = cmds[command];
}
}
}
const {getDecoratedKeymaps} = remote.require('./plugins');
let commands = {};
export const getRegisteredKeys = () => {
const keymaps = getDecoratedKeymaps();
return Object.keys(keymaps).reduce((result, actionName) => {
const commandKeys = keymaps[actionName];
commandKeys.forEach(shortcut => {
result[shortcut] = actionName;
});
return result;
}, {});
};
export const registerCommandHandlers = cmds => {
if (!cmds) {
return;
}
getCommand(cmd) {
return commands[cmd] !== undefined;
}
commands = Object.assign(commands, cmds);
};
exec(cmd, e) {
commands[cmd](e);
}
}
export const getCommandHandler = command => {
return commands[command];
};
export default new CommandRegistry();
// Some commands are directly excuted by Electron menuItem role.
// They should not be prevented to reach Electron.
const roleCommands = [
'window:close',
'editor:undo',
'editor:redo',
'editor:cut',
'editor:copy',
'editor:paste',
'editor:selectAll',
'window:minimize',
'window:zoom',
'window:toggleFullScreen'
];
export const shouldPreventDefault = command => !roleCommands.includes(command);

View file

@ -1,68 +0,0 @@
import React from 'react';
import {StyleSheet, css} from 'aphrodite-simple';
export default class Component extends React.PureComponent {
constructor() {
super();
this.styles_ = this.createStyleSheet();
this.cssHelper = this.cssHelper.bind(this);
}
createStyleSheet() {
if (!this.styles) {
return {};
}
const styles = this.styles();
if (typeof styles !== 'object') {
throw new TypeError('Component `styles` returns a non-object');
}
return StyleSheet.create(this.styles());
}
// wrap aphrodite's css helper for two reasons:
// - we can give the element an unaltered global classname
// that can be used to introduce global css side effects
// for example, through the configuration, web inspector
// or user agent extensions
// - the user doesn't need to keep track of both `css`
// and `style`, and we make that whole ordeal easier
cssHelper(...args) {
const classes = args
.map(c => {
if (c) {
// we compute the global name from the given
// css class and we prepend the component name
//
// it's important classes never get mangled by
// uglifiers so that we can avoid collisions
const component = this.constructor.name
.toString()
.toLowerCase();
const globalName = `${component}_${c}`;
return [globalName, css(this.styles_[c])];
}
return null;
})
// skip nulls
.filter(v => Boolean(v))
// flatten
.reduce((a, b) => a.concat(b));
return classes.length ? classes.join(' ') : null;
}
render() {
// convert static objects from `babel-plugin-transform-jsx`
// to `React.Element`.
if (!this.template) {
throw new TypeError('Component doesn\'t define `template`');
}
// invoke the template creator passing our css helper
return this.template(this.cssHelper);
}
}

View file

@ -1,14 +1,12 @@
import React from 'react';
import Component from '../component';
import {decorate, getTabsProps} from '../utils/plugins';
import Tabs_ from './tabs';
const Tabs = decorate(Tabs_, 'Tabs');
export default class Header extends Component {
export default class Header extends React.PureComponent {
constructor() {
super();
this.onChangeIntent = this.onChangeIntent.bind(this);
@ -22,8 +20,7 @@ export default class Header extends Component {
onChangeIntent(active) {
// we ignore clicks if they're a byproduct of a drag
// motion to move the window
if (window.screenX !== this.headerMouseDownWindowX ||
window.screenY !== this.headerMouseDownWindowY) {
if (window.screenX !== this.headerMouseDownWindowX || window.screenY !== this.headerMouseDownWindowY) {
return;
}
@ -74,11 +71,11 @@ export default class Header extends Component {
const {showHamburgerMenu, showWindowControls} = this.props;
const defaults = {
hambMenu: process.platform === 'win32', // show by default on windows
winCtrls: !this.props.isMac // show by default on windows and linux
hambMenu: !this.props.isMac, // show by default on windows and linux
winCtrls: !this.props.isMac // show by default on Windows and Linux
};
// don't allow the user to change defaults on MacOS
// don't allow the user to change defaults on macOS
if (this.props.isMac) {
return defaults;
}
@ -89,7 +86,7 @@ export default class Header extends Component {
};
}
template(css) {
render() {
const {isMac} = this.props;
const props = getTabsProps(this.props, {
tabs: this.props.tabs,
@ -105,148 +102,160 @@ export default class Header extends Component {
}
const {hambMenu, winCtrls} = this.getWindowHeaderConfig();
const left = winCtrls === 'left';
const maxButtonHref = this.props.maximized ?
'./renderer/assets/icons.svg#restore-window' :
'./renderer/assets/icons.svg#maximize-window';
const maxButtonHref = this.props.maximized
? './renderer/assets/icons.svg#restore-window'
: './renderer/assets/icons.svg#maximize-window';
return (<header
className={css('header', isMac && 'headerRounded')}
return (
<header
className={`header_header ${isMac && 'header_headerRounded'}`}
onMouseDown={this.handleHeaderMouseDown}
onDoubleClick={this.handleMaximizeClick}
>
{
!isMac &&
{!isMac && (
<div
className={css('windowHeader', props.tabs.length > 1 && 'windowHeaderWithBorder')}
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`}
style={{borderColor}}
>
{
hambMenu &&
{hambMenu && (
<svg
className={css('shape', (left && 'hamburgerMenuRight') || 'hamburgerMenuLeft')}
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`}
onClick={this.handleHamburgerMenuClick}
>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" />
</svg>
}
<span className={css('appTitle')}>{title}</span>
{
winCtrls &&
<div className={css('windowControls', left && 'windowControlsLeft')}>
)}
<span className="header_appTitle">{title}</span>
{winCtrls && (
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}>
<svg
className={css('shape', left && 'minimizeWindowLeft')}
className={`header_shape ${left ? 'header_minimizeWindowLeft' : ''}`}
onClick={this.handleMinimizeClick}
>
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" />
</svg>
<svg
className={css('shape', left && 'maximizeWindowLeft')}
className={`header_shape ${left ? 'header_maximizeWindowLeft' : ''}`}
onClick={this.handleMaximizeClick}
>
<use xlinkHref={maxButtonHref} />
</svg>
<svg
className={css('shape', 'closeWindow', left && 'closeWindowLeft')}
className={`header_shape header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`}
onClick={this.handleCloseClick}
>
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
}
)}
</div>
}
)}
{this.props.customChildrenBefore}
<Tabs {...props} />
{this.props.customChildren}
</header>);
<style jsx>{`
.header_header {
position: fixed;
top: 1px;
left: 1px;
right: 1px;
z-index: 100;
}
styles() {
return {
header: {
position: 'fixed',
top: '1px',
left: '1px',
right: '1px',
zIndex: '100'
},
headerRounded: {
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px'
},
windowHeader: {
height: '34px',
width: '100%',
position: 'fixed',
top: '1px',
left: '1px',
right: '1px',
WebkitAppRegion: 'drag',
WebkitUserSelect: 'none',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
windowHeaderWithBorder: {
borderColor: '#ccc',
borderBottomStyle: 'solid',
borderBottomWidth: '1px'
},
appTitle: {
fontSize: '12px'
},
shape: {
width: '40px',
height: '34px',
padding: '12px 15px 12px 15px',
WebkitAppRegion: 'no-drag',
color: '#FFFFFF',
opacity: 0.5,
shapeRendering: 'crispEdges',
':hover': {
opacity: 1
},
':active': {
opacity: 0.3
}
},
hamburgerMenuLeft: {
position: 'fixed',
top: '0',
left: '0'
},
hamburgerMenuRight: {
position: 'fixed',
top: '0',
right: '0'
},
windowControls: {
display: 'flex',
width: '120px',
height: '34px',
justifyContent: 'space-between',
position: 'fixed',
right: '0'
},
windowControlsLeft: {left: '0px'},
closeWindowLeft: {order: 1},
minimizeWindowLeft: {order: 2},
maximizeWindowLeft: {order: 3},
closeWindow: {':hover': {color: '#FE354E'}, ':active': {color: '#FE354E'}}
};
.header_headerRounded {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.header_windowHeader {
height: 34px;
width: 100%;
position: fixed;
top: 1px;
left: 1px;
right: 1px;
-webkit-app-region: drag;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.header_windowHeaderWithBorder {
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.header_appTitle {
font-size: 12px;
}
.header_shape {
width: 40px;
height: 34px;
padding: 12px 15px 12px 15px;
-webkit-app-region: no-drag;
color: #fff;
opacity: 0.5;
shape-rendering: crispEdges;
}
.header_shape:hover {
opacity: 1;
}
.header_shape:active {
opacity: 0.3;
}
.header_hamburgerMenuLeft {
position: fixed;
top: 0;
left: 0;
}
.header_hamburgerMenuRight {
position: fixed;
top: 0;
right: 0;
}
.header_windowControls {
display: flex;
width: 120px;
height: 34px;
justify-content: space-between;
position: fixed;
right: 0;
}
.header_windowControlsLeft {
left: 0px;
}
.header_closeWindowLeft {
order: 1;
}
.header_minimizeWindowLeft {
order: 2;
}
.header_maximizeWindowLeft {
order: 3;
}
.header_closeWindow:hover {
color: #fe354e;
}
.header_closeWindow:active {
color: #fe354e;
}
`}</style>
</header>
);
}
}

View file

@ -1,8 +1,6 @@
import React from 'react';
import Component from '../component';
export default class Notification extends Component {
export default class Notification extends React.PureComponent {
constructor() {
super();
this.state = {
@ -44,11 +42,7 @@ export default class Notification extends Component {
});
const {backgroundColor} = this.props;
if (backgroundColor) {
el.style.setProperty(
'background-color',
backgroundColor,
'important'
);
el.style.setProperty('background-color', backgroundColor, 'important');
}
}
}
@ -68,58 +62,53 @@ export default class Notification extends Component {
clearTimeout(this.dismissTimer);
}
template(css) {
render() {
const {backgroundColor} = this.props;
const opacity = this.state.dismissing ? 0 : 1;
return (<div
ref={this.onElement}
style={{opacity, backgroundColor}}
className={css('indicator')}
>
return (
<div ref={this.onElement} style={{opacity, backgroundColor}} className="notification_indicator">
{this.props.customChildrenBefore}
{this.props.children || this.props.text}
{
this.props.userDismissable ?
{this.props.userDismissable ? (
<a
className={css('dismissLink')}
className="notification_dismissLink"
onClick={this.handleDismiss}
style={{color: this.props.userDismissColor}}
>[x]</a> :
null
}
>
[x]
</a>
) : null}
{this.props.customChildren}
</div>);
<style jsx>{`
.notification_indicator {
display: inline-block;
cursor: default;
-webkit-user-select: none;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
padding: 8px 14px 9px;
margin-left: 10px;
transition: 150ms opacity ease;
color: #fff;
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
styles() {
return {
indicator: {
display: 'inline-block',
cursor: 'default',
WebkitUserSelect: 'none',
background: 'rgba(255, 255, 255, .2)',
borderRadius: '2px',
padding: '8px 14px 9px',
marginLeft: '10px',
transition: '150ms opacity ease',
color: '#fff',
fontSize: '11px',
fontFamily: `-apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif`
},
dismissLink: {
position: 'relative',
left: '4px',
cursor: 'pointer',
color: '#528D11',
':hover': {
color: '#2A5100'
}
}
};
.notification_dismissLink {
position: relative;
left: 4px;
cursor: pointer;
color: #528d11;
}
.notification_dismissLink:hover,
.notification_dismissLink:focus {
color: #2a5100;
}
`}</style>
</div>
);
}
}

View file

@ -1,19 +1,17 @@
import React from 'react';
import Component from '../component';
import {decorate} from '../utils/plugins';
import Notification_ from './notification';
const Notification = decorate(Notification_);
const Notification = decorate(Notification_, 'Notification');
export default class Notifications extends Component {
template(css) {
return (<div className={css('view')}>
export default class Notifications extends React.PureComponent {
render() {
return (
<div className="notifications_view">
{this.props.customChildrenBefore}
{
this.props.fontShowing &&
{this.props.fontShowing && (
<Notification
key="font"
backgroundColor="rgba(255, 255, 255, .2)"
@ -22,10 +20,9 @@ export default class Notifications extends Component {
onDismiss={this.props.onDismissFont}
dismissAfter={1000}
/>
}
)}
{
this.props.resizeShowing &&
{this.props.resizeShowing && (
<Notification
key="resize"
backgroundColor="rgba(255, 255, 255, .2)"
@ -34,10 +31,9 @@ export default class Notifications extends Component {
onDismiss={this.props.onDismissResize}
dismissAfter={1000}
/>
}
)}
{
this.props.messageShowing &&
{this.props.messageShowing && (
<Notification
key="message"
backgroundColor="#FE354E"
@ -45,8 +41,9 @@ export default class Notifications extends Component {
onDismiss={this.props.onDismissMessage}
userDismissable={this.props.messageDismissable}
userDismissColor="#AA2D3C"
>{
this.props.messageURL ? [
>
{this.props.messageURL
? [
this.props.messageText,
' (',
<a
@ -57,15 +54,16 @@ export default class Notifications extends Component {
ev.preventDefault();
}}
href={this.props.messageURL}
>more</a>,
>
more
</a>,
')'
] : null
}
]
: null}
</Notification>
}
)}
{
this.props.updateShowing &&
{this.props.updateShowing && (
<Notification
key="update"
backgroundColor="#7ED321"
@ -74,17 +72,17 @@ export default class Notifications extends Component {
userDismissable
>
Version <b>{this.props.updateVersion}</b> ready.
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`}
{' '}
(<a
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`} (<a
style={{color: '#fff'}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={`https://github.com/zeit/hyper/releases/tag/${this.props.updateVersion}`}
>notes</a>).
{' '}
>
notes
</a>).{' '}
{this.props.updateCanInstall ? (
<a
style={{
cursor: 'pointer',
@ -94,22 +92,36 @@ export default class Notifications extends Component {
onClick={this.props.onUpdateInstall}
>
Restart
</a>.
{ ' ' }
</a>
) : (
<a
style={{
color: '#fff',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={this.props.updateReleaseUrl}
>
Download
</a>
)}.{' '}
</Notification>
}
)}
{this.props.customChildren}
</div>);
}
styles() {
return {
view: {
position: 'fixed',
bottom: '20px',
right: '20px'
<style jsx>{`
.notifications_view {
position: fixed;
bottom: 20px;
right: 20px;
}
};
`}</style>
</div>
);
}
}

View file

@ -1,12 +1,12 @@
/* eslint-disable quote-props */
import React from 'react';
import Component from '../component';
export default class SplitPane extends Component {
import _ from 'lodash';
export default class SplitPane extends React.PureComponent {
constructor(props) {
super(props);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleAutoResize = this.handleAutoResize.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.state = {dragging: false};
@ -19,6 +19,28 @@ export default class SplitPane extends Component {
}
}
setupPanes(ev) {
this.panes = Array.from(ev.target.parentNode.childNodes);
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
}
handleAutoResize(ev) {
ev.preventDefault();
this.setupPanes(ev);
const sizes_ = this.getSizes();
sizes_[this.paneIndex] = 0;
sizes_[this.paneIndex + 1] = 0;
const availableWidth = 1 - _.sum(sizes_);
sizes_[this.paneIndex] = availableWidth / 2;
sizes_[this.paneIndex + 1] = availableWidth / 2;
this.props.onResize(sizes_);
}
handleDragStart(ev) {
ev.preventDefault();
this.setState({dragging: true});
@ -38,14 +60,12 @@ export default class SplitPane extends Component {
this.dragTarget = ev.target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
this.panes = Array.from(ev.target.parentNode.childNodes);
this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1];
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
this.setupPanes(ev);
}
onDrag(ev) {
let {sizes} = this.props;
getSizes() {
const {sizes} = this.props;
let sizes_;
if (sizes) {
@ -54,9 +74,13 @@ export default class SplitPane extends Component {
const total = this.props.children.length;
const count = new Array(total).fill(1 / total);
sizes = count;
sizes_ = count;
}
return sizes_;
}
onDrag(ev) {
const sizes_ = this.getSizes();
const i = this.paneIndex;
const pos = ev[this.d3];
@ -79,9 +103,10 @@ export default class SplitPane extends Component {
}
}
template(css) {
render() {
const children = this.props.children;
const {direction, borderColor} = this.props;
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
let {sizes} = this.props;
if (!sizes) {
// workaround for the fact that if we don't specify
@ -89,101 +114,96 @@ export default class SplitPane extends Component {
// right height for the horizontal panes
sizes = new Array(children.length).fill(1 / children.length);
}
return (<div className={css('panes', `panes_${direction}`)}>
{
React.Children.map(children, (child, i) => {
return (
<div className={`splitpane_panes splitpane_panes_${direction}`}>
{React.Children.map(children, (child, i) => {
const style = {
flexBasis: (sizes[i] * 100) + '%',
// flexBasis doesn't work for the first horizontal pane, height need to be specified
[sizeProperty]: sizes[i] * 100 + '%',
flexBasis: sizes[i] * 100 + '%',
flexGrow: 0
};
return [
<div
key="pane"
className={css('pane')}
style={style}
>
<div key="pane" className="splitpane_pane" style={style}>
{child}
</div>,
i < children.length - 1 ?
i < children.length - 1 ? (
<div
key="divider"
onMouseDown={this.handleDragStart}
onDoubleClick={this.handleAutoResize}
style={{backgroundColor: borderColor}}
className={css('divider', `divider_${direction}`)}
/> :
null
];
})
}
<div
style={{display: this.state.dragging ? 'block' : 'none'}}
className={css('shim')}
className={`splitpane_divider splitpane_divider_${direction}`}
/>
</div>);
) : null
];
})}
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />
<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
styles() {
return {
panes: {
display: 'flex',
flex: 1,
outline: 'none',
position: 'relative',
width: '100%',
height: '100%'
},
'panes_vertical': {
flexDirection: 'row'
},
'panes_horizontal': {
flexDirection: 'column'
},
pane: {
flex: 1,
outline: 'none',
position: 'relative'
},
divider: {
boxSizing: 'border-box',
zIndex: '1',
backgroundClip: 'padding-box',
flexShrink: 0
},
'divider_vertical': {
borderLeft: '5px solid rgba(255, 255, 255, 0)',
borderRight: '5px solid rgba(255, 255, 255, 0)',
width: '11px',
margin: '0 -5px',
cursor: 'col-resize'
},
'divider_horizontal': {
height: '11px',
margin: '-5px 0',
borderTop: '5px solid rgba(255, 255, 255, 0)',
borderBottom: '5px solid rgba(255, 255, 255, 0)',
cursor: 'row-resize',
width: '100%'
},
// this shim is used to make sure mousemove events
// trigger in all the draggable area of the screen
//
// this is not the case due to hterm's <iframe>
shim: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'transparent'
.splitpane_panes_vertical {
flex-direction: row;
}
};
.splitpane_panes_horizontal {
flex-direction: column;
}
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
/*
this shim is used to make sure mousemove events
trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
}
componentWillUnmount() {
@ -192,5 +212,4 @@ export default class SplitPane extends Component {
this.onDragEnd();
}
}
}

View file

@ -0,0 +1,144 @@
import React from 'react';
export default class StyleSheet extends React.PureComponent {
render() {
const {backgroundColor, fontFamily, foregroundColor, borderColor} = this.props;
return (
<style jsx global>{`
.xterm {
font-family: ${fontFamily};
font-feature-settings: 'liga' 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 10;
}
.xterm .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -10;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: ${backgroundColor};
color: ${foregroundColor};
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: ${backgroundColor};
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm .xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
left: -9999em;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm:not(.enable-mouse-events) {
cursor: text;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .xterm-accessibility-tree:focus [id^='xterm-active-item-'] {
outline: 1px solid #f80;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
background: ${borderColor};
}
::-webkit-scrollbar-thumb:window-inactive {
background: ${borderColor};
}
`}</style>
);
}
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import Component from '../component';
export default class Tab extends Component {
export default class Tab extends React.PureComponent {
constructor() {
super();
@ -14,10 +13,6 @@ export default class Tab extends Component {
};
}
shouldComponentUpdate() {
return true;
}
handleHover() {
this.setState({
hovered: true
@ -41,160 +36,145 @@ export default class Tab extends Component {
}
}
template(css) {
render() {
const {isActive, isFirst, isLast, borderColor, hasActivity} = this.props;
const {hovered} = this.state;
return (<li
return (
<React.Fragment>
<li
onMouseEnter={this.handleHover}
onMouseLeave={this.handleBlur}
onClick={this.props.onClick}
style={{borderColor}}
className={css(
'tab',
isFirst && 'first',
isActive && 'active',
isFirst && isActive && 'firstActive',
hasActivity && 'hasActivity'
)}
className={`tab_tab ${isFirst ? 'tab_first' : ''} ${isActive ? 'tab_active' : ''} ${
isFirst && isActive ? 'tab_firstActive' : ''
} ${hasActivity ? 'tab_hasActivity' : ''}`}
>
{this.props.customChildrenBefore}
<span
className={css(
'text',
isLast && 'textLast',
isActive && 'textActive'
)}
className={`tab_text ${isLast ? 'tab_textLast' : ''} ${isActive ? 'tab_textActive' : ''}`}
onClick={this.handleClick}
>
<span
title={this.props.text}
className={css('textInner')}
>
<span title={this.props.text} className="tab_textInner">
{this.props.text}
</span>
</span>
<i
className={css(
'icon',
hovered && 'iconHovered'
)}
onClick={this.props.onClose}
>
<svg className={css('shape')}>
<i className={`tab_icon ${hovered ? 'tab_iconHovered' : ''}`} onClick={this.props.onClose}>
<svg className="tab_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-tab" />
</svg>
</i>
{this.props.customChildren}
</li>);
</li>
<style jsx>{`
.tab_tab {
color: #ccc;
border-color: #ccc;
border-bottom-width: 1px;
border-bottom-style: solid;
border-left-width: 1px;
border-left-style: solid;
list-style-type: none;
flex-grow: 1;
position: relative;
}
styles() {
return {
tab: {
color: '#ccc',
borderColor: '#ccc',
borderBottomWidth: 1,
borderBottomStyle: 'solid',
borderLeftWidth: 1,
borderLeftStyle: 'solid',
listStyleType: 'none',
flexGrow: 1,
position: 'relative',
':hover': {
color: '#ccc'
}
},
first: {
borderLeftWidth: 0,
paddingLeft: 1
},
firstActive: {
borderLeftWidth: 1,
paddingLeft: 0
},
active: {
color: '#fff',
borderBottomWidth: 0,
':hover': {
color: '#fff'
}
},
hasActivity: {
color: '#50E3C2',
':hover': {
color: '#50E3C2'
}
},
text: {
transition: 'color .2s ease',
height: '34px',
display: 'block',
width: '100%',
position: 'relative',
overflow: 'hidden'
},
textInner: {
position: 'absolute',
left: '24px',
right: '24px',
top: 0,
bottom: 0,
textAlign: 'center',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden'
},
icon: {
transition: `opacity .2s ease, color .2s ease,
transform .25s ease, background-color .1s ease`,
pointerEvents: 'none',
position: 'absolute',
right: '7px',
top: '10px',
display: 'inline-block',
width: '14px',
height: '14px',
borderRadius: '100%',
color: '#e9e9e9',
opacity: 0,
transform: 'scale(.95)',
':hover': {
backgroundColor: 'rgba(255,255,255, .13)',
color: '#fff'
},
':active': {
backgroundColor: 'rgba(255,255,255, .1)',
color: '#909090'
}
},
iconHovered: {
opacity: 1,
transform: 'none',
pointerEvents: 'all'
},
shape: {
position: 'absolute',
left: '4px',
top: '4px',
width: '6px',
height: '6px',
verticalAlign: 'middle',
fill: 'currentColor',
shapeRendering: 'crispEdges'
}
};
.tab_tab:hover {
color: #ccc;
}
.tab_first {
border-left-width: 0;
padding-left: 1px;
}
.tab_firstActive {
border-left-width: 1px;
padding-left: 0;
}
.tab_active {
color: #fff;
border-bottom-width: 0;
}
.tab_active:hover {
color: #fff;
}
.tab_hasActivity {
color: #50e3c2;
}
.tab_hasActivity:hover {
color: #50e3c2;
}
.tab_text {
transition: color 0.2s ease;
height: 34px;
display: block;
width: 100%;
position: relative;
overflow: hidden;
}
.tab_textInner {
position: absolute;
left: 24px;
right: 24px;
top: 0;
bottom: 0;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.tab_icon {
transition: opacity 0.2s ease, color 0.2s ease, transform 0.25s ease, background-color 0.1s ease;
pointer-events: none;
position: absolute;
right: 7px;
top: 10px;
display: inline-block;
width: 14px;
height: 14px;
border-radius: 100%;
color: #e9e9e9;
opacity: 0;
transform: scale(0.95);
}
.tab_icon:hover {
background-color: rgba(255, 255, 255, 0.13);
color: #fff;
}
.tab_icon:active {
background-color: rgba(255, 255, 255, 0.1);
color: #909090;
}
.tab_iconHovered {
opacity: 1;
transform: none;
pointer-events: all;
}
.tab_shape {
position: absolute;
left: 4px;
top: 4px;
width: 6px;
height: 6px;
vertical-align: middle;
fill: currentColor;
shape-rendering: crispEdges;
}
`}</style>
</React.Fragment>
);
}
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import Component from '../component';
import {decorate, getTabProps} from '../utils/plugins';
import Tab_ from './tab';
@ -8,34 +7,20 @@ import Tab_ from './tab';
const Tab = decorate(Tab_, 'Tab');
const isMac = /Mac/.test(navigator.userAgent);
export default class Tabs extends Component {
template(css) {
const {
tabs = [],
borderColor,
onChange,
onClose
} = this.props;
export default class Tabs extends React.PureComponent {
render() {
const {tabs = [], borderColor, onChange, onClose} = this.props;
const hide = !isMac && tabs.length === 1;
return (<nav className={css('nav', hide && 'hiddenNav')}>
return (
<nav className={`tabs_nav ${hide ? 'tabs_hiddenNav' : ''}`}>
{this.props.customChildrenBefore}
{
tabs.length === 1 && isMac ?
<div className={css('title')}>{tabs[0].title}</div> :
null
}
{
tabs.length > 1 ?
[
<ul
key="list"
className={css('list')}
>
{
tabs.map((tab, i) => {
{tabs.length === 1 && isMac ? <div className="tabs_title">{tabs[0].title}</div> : null}
{tabs.length > 1
? [
<ul key="list" className="tabs_list">
{tabs.map((tab, i) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
@ -48,66 +33,58 @@ export default class Tabs extends Component {
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props} />;
})
}
})}
</ul>,
isMac && <div
key="shim"
style={{borderColor}}
className={css('borderShim')}
/>
] :
null
}
isMac && <div key="shim" style={{borderColor}} className="tabs_borderShim" />
]
: null}
{this.props.customChildren}
</nav>);
<style jsx>{`
.tabs_nav {
font-size: 12px;
height: 34px;
line-height: 34px;
vertical-align: middle;
color: #9b9b9b;
cursor: default;
position: relative;
-webkit-user-select: none;
-webkit-app-region: ${isMac ? 'drag' : ''};
top: ${isMac ? '0px' : '34px'};
}
styles() {
return {
nav: {
fontSize: '12px',
height: '34px',
lineHeight: '34px',
verticalAlign: 'middle',
color: '#9B9B9B',
cursor: 'default',
position: 'relative',
WebkitUserSelect: 'none',
WebkitAppRegion: isMac ? 'drag' : '',
top: isMac ? '0px' : '34px'
},
hiddenNav: {
display: 'none'
},
title: {
textAlign: 'center',
color: '#fff',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
paddingLeft: 76,
paddingRight: 76
},
list: {
maxHeight: '34px',
display: 'flex',
flexFlow: 'row',
marginLeft: isMac ? 76 : 0
},
borderShim: {
position: 'absolute',
width: '76px',
bottom: 0,
borderColor: '#ccc',
borderBottomStyle: 'solid',
borderBottomWidth: '1px'
}
};
.tabs_hiddenNav {
display: none;
}
.tabs_title {
text-align: center;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 76px;
padding-right: 76px;
}
.tabs_list {
max-height: 34px;
display: flex;
flex-flow: row;
margin-left: ${isMac ? '76px' : '0'};
}
.tabs_borderShim {
position: absolute;
width: 76px;
bottom: 0;
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
`}</style>
</nav>
);
}
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import {connect} from 'react-redux';
import Component from '../component';
import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins';
import {resizeTermGroup} from '../actions/term-groups';
import Term_ from './term';
@ -9,11 +8,13 @@ import SplitPane_ from './split-pane';
const Term = decorate(Term_, 'Term');
const SplitPane = decorate(SplitPane_, 'SplitPane');
class TermGroup_ extends Component {
class TermGroup_ extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.bound = new WeakMap();
this.termRefs = {};
this.sizeChanged = false;
this.onTermRef = this.onTermRef.bind(this);
}
bind(fn, thisObj, uid) {
@ -34,14 +35,21 @@ class TermGroup_ extends Component {
}
const direction = this.props.termGroup.direction.toLowerCase();
return (<SplitPane
return (
<SplitPane
direction={direction}
sizes={this.props.termGroup.sizes}
onResize={this.props.onTermGroupResize}
borderColor={this.props.borderColor}
>
{groups}
</SplitPane>);
</SplitPane>
);
}
onTermRef(uid, term) {
this.term = term;
this.props.ref_(uid, term);
}
renderTerm(uid) {
@ -50,19 +58,21 @@ class TermGroup_ extends Component {
const props = getTermProps(uid, this.props, {
isTermActive: uid === this.props.activeSession,
term: termRef ? termRef.term : null,
customCSS: this.props.customCSS,
fontSize: this.props.fontSize,
cursorColor: this.props.cursorColor,
cursorShape: this.props.cursorShape,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
colors: this.props.colors,
cursorBlink: this.props.cursorBlink,
cursorShape: this.props.cursorShape,
cursorColor: this.props.cursorColor,
cursorAccentColor: this.props.cursorAccentColor,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
fontWeight: this.props.fontWeight,
fontWeightBold: this.props.fontWeightBold,
modifierKeys: this.props.modifierKeys,
padding: this.props.padding,
colors: this.props.colors,
url: session.url,
cleared: session.cleared,
cols: session.cols,
@ -75,7 +85,9 @@ class TermGroup_ extends Component {
onTitle: this.bind(this.props.onTitle, null, uid),
onData: this.bind(this.props.onData, null, uid),
onURLAbort: this.bind(this.props.onURLAbort, null, uid),
onContextMenu: this.bind(this.props.onContextMenu, null, uid),
borderColor: this.props.borderColor,
selectionColor: this.props.selectionColor,
quickEdit: this.props.quickEdit,
uid
});
@ -83,28 +95,36 @@ class TermGroup_ extends Component {
// This will create a new ref_ function for every render,
// which is inefficient. Should maybe do something similar
// to this.bind.
return (<Term
key={uid}
ref_={term => this.props.ref_(uid, term)}
{...props}
/>);
return <Term ref_={this.onTermRef} key={uid} {...props} />;
}
template() {
componentWillReceiveProps(nextProps) {
if (this.props.termGroup.sizes != nextProps.termGroup.sizes || nextProps.sizeChanged) {
this.term && this.term.fitResize();
// Indicate to children that their size has changed even if their ratio hasn't
this.sizeChanged = true;
} else {
this.sizeChanged = false;
}
}
render() {
const {childGroups, termGroup} = this.props;
if (termGroup.sessionUid) {
return this.renderTerm(termGroup.sessionUid);
}
const groups = childGroups.map(child => {
const props = getTermGroupProps(child.uid, this.props.parentProps, Object.assign({}, this.props, {
termGroup: child
}));
const props = getTermGroupProps(
child.uid,
this.props.parentProps,
Object.assign({}, this.props, {
termGroup: child,
sizeChanged: this.sizeChanged
})
);
return (<DecoratedTermGroup
key={child.uid}
{...props}
/>);
return <DecoratedTermGroup key={child.uid} {...props} />;
});
return this.renderSplit(groups);
@ -113,9 +133,7 @@ class TermGroup_ extends Component {
const TermGroup = connect(
(state, ownProps) => ({
childGroups: ownProps.termGroup.children.map(uid =>
state.termGroups.termGroups[uid]
)
childGroups: ownProps.termGroup.children.map(uid => state.termGroups.termGroups[uid])
}),
(dispatch, ownProps) => ({
onTermGroupResize(splitSizes) {

View file

@ -1,437 +1,335 @@
/* global Blob,URL,requestAnimationFrame */
import React from 'react';
import Color from 'color';
import uuid from 'uuid';
import hterm from '../hterm';
import Component from '../component';
import getColorList from '../utils/colors';
import {Terminal} from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat';
import {clipboard} from 'electron';
import * as Color from 'color';
import terms from '../terms';
import notify from '../utils/notify';
import processClipboard from '../utils/paste';
export default class Term extends Component {
Terminal.applyAddon(fit);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
// map old hterm constants to xterm.js
const CURSOR_STYLES = {
BEAM: 'bar',
UNDERLINE: 'underline',
BLOCK: 'block'
};
const getTermOptions = props => {
// Set a background color only if it is opaque
const needTransparency = Color(props.backgroundColor).alpha() < 1;
const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor;
return {
macOptionIsMeta: props.modifierKeys.altIsMeta,
cursorStyle: CURSOR_STYLES[props.cursorShape],
cursorBlink: props.cursorBlink,
fontFamily: props.fontFamily,
fontSize: props.fontSize,
fontWeight: props.fontWeight,
fontWeightBold: props.fontWeightBold,
allowTransparency: needTransparency,
experimentalCharAtlas: 'dynamic',
theme: {
foreground: props.foregroundColor,
background: backgroundColor,
cursor: props.cursorColor,
cursorAccent: props.cursorAccentColor,
selection: props.selectionColor,
black: props.colors.black,
red: props.colors.red,
green: props.colors.green,
yellow: props.colors.yellow,
blue: props.colors.blue,
magenta: props.colors.magenta,
cyan: props.colors.cyan,
white: props.colors.white,
brightBlack: props.colors.lightBlack,
brightRed: props.colors.lightRed,
brightGreen: props.colors.lightGreen,
brightYellow: props.colors.lightYellow,
brightBlue: props.colors.lightBlue,
brightMagenta: props.colors.lightMagenta,
brightCyan: props.colors.lightCyan,
brightWhite: props.colors.lightWhite
}
};
};
export default class Term extends React.PureComponent {
constructor(props) {
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.onHyperCaret = this.onHyperCaret.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleFocus = this.handleFocus.bind(this);
props.ref_(this);
props.ref_(props.uid, this);
this.termRef = null;
this.termWrapperRef = null;
this.termRect = null;
this.onOpen = this.onOpen.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onWindowPaste = this.onWindowPaste.bind(this);
this.onTermRef = this.onTermRef.bind(this);
this.onTermWrapperRef = this.onTermWrapperRef.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.termOptions = {};
}
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
if (props.cols && props.rows) {
this.term.realizeSize_(props.cols, props.rows);
this.termOptions = getTermOptions(props);
this.term = props.term || new Terminal(this.termOptions);
this.term.attachCustomKeyEventHandler(this.keyboardHandler);
this.term.open(this.termRef);
this.term.webLinksInit();
this.term.winptyCompatInit();
if (props.term) {
//We need to set options again after reattaching an existing term
Object.keys(this.termOptions).forEach(option => this.term.setOption(option, this.termOptions[option]));
}
if (this.props.isTermActive) {
this.term.focus();
}
const prefs = this.term.getPrefs();
this.onOpen(this.termOptions);
prefs.set('font-family', props.fontFamily);
prefs.set('font-size', props.fontSize);
prefs.set('font-smoothing', props.fontSmoothing);
prefs.set('cursor-color', this.validateColor(props.cursorColor, 'rgba(255,255,255,0.5)'));
prefs.set('cursor-blink', props.cursorBlink);
prefs.set('enable-clipboard-notice', false);
prefs.set('foreground-color', props.foregroundColor);
// hterm.ScrollPort.prototype.setBackgroundColor is overriden
// to make hterm's background transparent. we still need to set
// background-color for proper text rendering
prefs.set('background-color', props.backgroundColor);
prefs.set('color-palette-overrides', getColorList(props.colors));
prefs.set('user-css', this.getStylesheet(props.customCSS));
prefs.set('scrollbar-visible', false);
prefs.set('receive-encoding', 'raw');
prefs.set('send-encoding', 'raw');
prefs.set('alt-sends-what', 'browser-key');
if (props.bell === 'SOUND') {
prefs.set('audible-bell-sound', this.props.bellSoundURL);
} else {
prefs.set('audible-bell-sound', '');
if (props.onTitle) {
this.term.on('title', props.onTitle);
}
if (props.copyOnSelect) {
prefs.set('copy-on-select', true);
} else {
prefs.set('copy-on-select', false);
if (props.onActive) {
this.term.on('focus', props.onActive);
}
this.term.onTerminalReady = () => {
const io = this.term.io.push();
if (props.onData) {
this.term.on('data', props.onData);
}
io.onVTKeystroke = props.onData;
io.sendString = props.onData;
io.onTerminalResize = (cols, rows) => {
if (cols !== this.props.cols || rows !== this.props.rows) {
if (props.onResize) {
this.term.on('resize', ({cols, rows}) => {
props.onResize(cols, rows);
}
};
this.term.modifierKeys = props.modifierKeys;
// this.term.CursorNode_ is available at this point.
this.term.setCursorShape(props.cursorShape);
// required to be set for CursorBlink to work
this.term.setCursorVisible(true);
// 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();
if (this.props.onTerminal) {
this.props.onTerminal(this.term);
});
}
const iframeWindow = this.getTermDocument().defaultView;
iframeWindow.addEventListener('wheel', this.handleWheel);
if (props.onCursorMove) {
this.term.on('cursormove', () => {
const cursorFrame = {
x: this.term.buffer.x * this.term.renderer.dimensions.actualCellWidth,
y: this.term.buffer.y * this.term.renderer.dimensions.actualCellHeight,
width: this.term.renderer.dimensions.actualCellWidth,
height: this.term.renderer.dimensions.actualCellHeight,
col: this.term.buffer.y,
row: this.term.buffer.x
};
props.onCursorMove(cursorFrame);
});
}
window.addEventListener('resize', this.onWindowResize, {
passive: true
});
window.addEventListener('paste', this.onWindowPaste, {
capture: true
});
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
this.getScreenNode().addEventListener('mousedown', this.handleMouseDown);
terms[this.props.uid] = this;
}
handleWheel(e) {
if (this.props.onWheel) {
this.props.onWheel(e);
onOpen(termOptions) {
// we need to delay one frame so that styles
// get applied and we can make an accurate measurement
// of the container width and height
requestAnimationFrame(() => {
// at this point it would make sense for character
// measurement to have taken place but it seems that
// xterm.js might be doing this asynchronously, so
// we force it instead
// eslint-disable-next-line no-debugger
//debugger;
this.term.charMeasure.measure(termOptions);
this.fitResize();
});
}
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', true);
clearTimeout(this.scrollbarsHideTimer);
if (!this.scrollMouseEnter) {
this.scrollbarsHideTimer = setTimeout(() => {
prefs.set('scrollbar-visible', false);
}, 1000);
getTermDocument() {
// eslint-disable-next-line no-console
console.warn(
'The underlying terminal engine of Hyper no longer ' +
'uses iframes with individual `document` objects for each ' +
'terminal instance. This method call is retained for ' +
"backwards compatibility reasons. It's ok to attach directly" +
'to the `document` object of the main `window`.'
);
return document;
}
onWindowResize() {
this.fitResize();
}
// intercepting paste event for any necessary processing of
// clipboard data, if result is falsy, paste event continues
onWindowPaste(e) {
if (!this.props.isTermActive) return;
const processed = processClipboard();
if (processed) {
e.preventDefault();
e.stopPropagation();
this.term.send(processed);
}
}
handleScrollEnter() {
clearTimeout(this.scrollbarsHideTimer);
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', true);
this.scrollMouseEnter = true;
onMouseUp(e) {
if (this.props.quickEdit && e.button === 2) {
if (this.term.hasSelection()) {
clipboard.writeText(this.term.getSelection());
this.term.clearSelection();
} else {
document.execCommand('paste');
}
handleScrollLeave() {
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', false);
this.scrollMouseEnter = false;
} else if (this.props.copyOnSelect && this.term.hasSelection()) {
clipboard.writeText(this.term.getSelection());
}
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();
}
}
handleFocus() {
// This will in turn result in `this.focus()` being
// called, which is unecessary.
// Should investigate if it matters.
this.props.onActive();
}
handleKeyDown(e) {
if (e.ctrlKey && e.key === 'c') {
this.props.onURLAbort();
}
}
onHyperCaret(caret) {
this.hyperCaret = caret;
}
write(data) {
// sometimes the preference set above for
// `receive-encoding` is not known by the vt
// before we type to write (since the preference
// manager is asynchronous), so we force it to
// avoid buffering
// this fixes a race condition where sometimes
// opening new sessions results in broken
// output due to the term attempting to decode
// as `utf-8` instead of `raw`
if (this.term.vt.characterEncoding !== 'raw') {
this.term.vt.characterEncoding = 'raw';
}
this.term.io.writeUTF8(data);
this.term.write(data);
}
focus() {
this.term.focusHyperCaret();
this.term.focus();
}
clear() {
this.term.wipeContents();
this.term.onVTKeystroke('\f');
this.term.clear();
}
moveWordLeft() {
this.term.onVTKeystroke('\x1bb');
reset() {
this.term.reset();
}
moveWordRight() {
this.term.onVTKeystroke('\x1bf');
}
deleteWordLeft() {
this.term.onVTKeystroke('\x1b\x7f');
}
deleteWordRight() {
this.term.onVTKeystroke('\x1bd');
}
deleteLine() {
this.term.onVTKeystroke('\x1bw');
}
moveToStart() {
this.term.onVTKeystroke('\x01');
}
moveToEnd() {
this.term.onVTKeystroke('\x05');
resize(cols, rows) {
this.term.resize(cols, rows);
}
selectAll() {
this.term.selectAll();
}
getScreenNode() {
return this.term.scrollPort_.getScreenNode();
fitResize() {
if (!this.termWrapperRef) {
return;
}
this.term.fit();
}
getTermDocument() {
return this.term.document_;
}
getStylesheet(css) {
const hyperCaret = `
.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;
}
`;
const scrollBarCss = `
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
background: ${this.props.borderColor};
}
::-webkit-scrollbar-thumb:window-inactive {
background: ${this.props.borderColor};
}
`;
const selectCss = `
::selection {
background: ${Color(this.props.cursorColor).alpha(0.4).rgb().toString()};
}
`;
return URL.createObjectURL(new Blob([`
.cursor-node[focus="false"] {
border-width: 1px !important;
}
x-row {
line-height: 1em;
}
${hyperCaret}
${scrollBarCss}
${selectCss}
${css}
`], {type: 'text/css'}));
}
validateColor(color, alternative = 'rgb(255,255,255)') {
try {
return Color(color).rgb().toString();
} catch (err) {
notify(`color "${color}" is invalid`);
}
return alternative;
}
handleMouseDown(ev) {
// we prevent losing focus when clicking the boundary
// wrappers of the main terminal element
if (ev.target === this.termWrapperRef ||
ev.target === this.termRef) {
ev.preventDefault();
}
if (this.props.quickEdit) {
this.term.onMouseDown_(ev);
}
keyboardHandler(e) {
// Has Mousetrap flagged this event as a command?
return !e.catched;
}
componentWillReceiveProps(nextProps) {
if (this.props.url !== nextProps.url) {
// when the url prop changes, we make sure
// the terminal starts or stops ignoring
// key input so that it doesn't conflict
// with the <webview>
if (nextProps.url) {
this.term.io.push();
window.addEventListener('keydown', this.handleKeyDown);
} else {
window.removeEventListener('keydown', this.handleKeyDown);
this.term.io.pop();
}
}
if (!this.props.cleared && nextProps.cleared) {
this.clear();
}
const nextTermOptions = getTermOptions(nextProps);
const prefs = this.term.getPrefs();
// Update only options that have changed.
Object.keys(nextTermOptions)
.filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
.forEach(option => this.term.setOption(option, nextTermOptions[option]));
if (this.props.fontSize !== nextProps.fontSize) {
prefs.set('font-size', nextProps.fontSize);
this.hyperCaret.style.fontSize = nextProps.fontSize + 'px';
// Do we need to update theme?
const shouldUpdateTheme =
!this.termOptions.theme ||
Object.keys(nextTermOptions.theme).some(
option => nextTermOptions.theme[option] !== this.termOptions.theme[option]
);
if (shouldUpdateTheme) {
this.term.setOption('theme', nextTermOptions.theme);
}
if (this.props.foregroundColor !== nextProps.foregroundColor) {
prefs.set('foreground-color', nextProps.foregroundColor);
this.termOptions = nextTermOptions;
if (!this.props.isTermActive && nextProps.isTermActive) {
requestAnimationFrame(() => {
this.term.charMeasure.measure(this.termOptions);
this.fitResize();
});
}
if (this.props.fontFamily !== nextProps.fontFamily) {
prefs.set('font-family', nextProps.fontFamily);
this.hyperCaret.style.fontFamily = nextProps.fontFamily;
if (this.props.fontSize !== nextProps.fontSize || this.props.fontFamily !== nextProps.fontFamily) {
// invalidate xterm cache about how wide each
// character is
this.term.charMeasure.measure(this.termOptions);
// resize to fit the container
this.fitResize();
}
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
prefs.set('font-smoothing', nextProps.fontSmoothing);
if (nextProps.rows !== this.props.rows || nextProps.cols !== this.props.cols) {
this.resize(nextProps.cols, nextProps.rows);
}
}
if (this.props.cursorColor !== nextProps.cursorColor) {
prefs.set('cursor-color', this.validateColor(nextProps.cursorColor, 'rgba(255,255,255,0.5)'));
onTermWrapperRef(component) {
this.termWrapperRef = component;
}
if (this.props.cursorShape !== nextProps.cursorShape) {
this.term.setCursorShape(nextProps.cursorShape);
}
if (this.props.cursorBlink !== nextProps.cursorBlink) {
prefs.set('cursor-blink', nextProps.cursorBlink);
}
if (this.props.colors !== nextProps.colors) {
prefs.set('color-palette-overrides', getColorList(nextProps.colors));
}
if (this.props.customCSS !== nextProps.customCSS) {
prefs.set('user-css', this.getStylesheet(nextProps.customCSS));
}
if (this.props.bell === 'SOUND') {
prefs.set('audible-bell-sound', this.props.bellSoundURL);
} else {
prefs.set('audible-bell-sound', '');
}
if (this.props.copyOnSelect) {
prefs.set('copy-on-select', true);
} else {
prefs.set('copy-on-select', false);
}
onTermRef(component) {
this.termRef = component;
}
componentWillUnmount() {
terms[this.props.uid] = this;
// turn blinking off to prevent leaking a timeout when disposing terminal
const prefs = this.term.getPrefs();
prefs.set('cursor-blink', false);
clearTimeout(this.scrollbarsHideTimer);
this.props.ref_(null);
terms[this.props.uid] = null;
this.props.ref_(this.props.uid, null);
// to clean up the terminal, we remove the listeners
// instead of invoking `destroy`, since it will make the
// term insta un-attachable in the future (which we need
// to do in case of splitting, see `componentDidMount`
['title', 'focus', 'data', 'resize', 'cursormove'].forEach(type => this.term.removeAllListeners(type));
window.removeEventListener('resize', this.onWindowResize, {
passive: true
});
window.removeEventListener('paste', this.onWindowPaste, {
capture: true
});
}
template(css) {
return (<div
ref={component => {
this.termWrapperRef = component;
}}
className={css('fit', this.props.isTermActive && 'active')}
onMouseDown={this.handleMouseDown}
render() {
return (
<div
className={`term_fit ${this.props.isTermActive ? 'term_active' : ''}`}
style={{padding: this.props.padding}}
onMouseUp={this.onMouseUp}
>
{this.props.customChildrenBefore}
<div
ref={component => {
this.termRef = component;
}}
className={css('fit', 'term')}
/>
{ this.props.url ?
<webview
key="hyper-webview"
src={this.props.url}
onFocus={this.handleFocus}
style={{
background: '#fff',
position: 'absolute',
top: 0,
left: 0,
display: 'inline-flex',
width: '100%',
height: '100%'
}}
/> :
<div // eslint-disable-line react/jsx-indent
key="scrollbar"
className={css('scrollbarShim')}
onMouseEnter={this.handleScrollEnter}
onMouseLeave={this.handleScrollLeave}
/>
}
<div key="hyper-caret" ref={this.onHyperCaret} contentEditable className="hyper-caret"/>
<div ref={this.onTermWrapperRef} className="term_fit term_wrapper">
<div ref={this.onTermRef} className="term_fit term_term" />
</div>
{this.props.customChildren}
</div>);
<style jsx>{`
.term_fit {
display: block;
width: 100%;
height: 100%;
}
styles() {
return {
fit: {
display: 'block',
width: '100%',
height: '100%'
},
term: {
position: 'relative'
},
scrollbarShim: {
position: 'fixed',
right: 0,
width: '50px',
top: 0,
bottom: 0,
pointerEvents: 'none'
.term_wrapper {
/* TODO: decide whether to keep this or not based on understanding what xterm-selection is for */
overflow: hidden;
}
};
`}</style>
</div>
);
}
}

View file

@ -1,20 +1,21 @@
import React from 'react';
import Component from '../component';
import {decorate, getTermGroupProps} from '../utils/plugins';
import CommandRegistry from '../command-registry';
import {registerCommandHandlers} from '../command-registry';
import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet';
const TermGroup = decorate(TermGroup_, 'TermGroup');
const StyleSheet = decorate(StyleSheet_, 'StyleSheet');
const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends Component {
export default class Terms extends React.Component {
constructor(props, context) {
super(props, context);
this.terms = {};
this.bound = new WeakMap();
this.onRef = this.onRef.bind(this);
this.registerCommands = CommandRegistry.register;
this.registerCommands = registerCommandHandlers;
props.ref_(this);
}
@ -62,18 +63,24 @@ export default class Terms extends Component {
this.terms[uid] = term;
}
componentDidMount() {
window.addEventListener('contextmenu', () => {
const selection = window.getSelection().toString();
const {props: {uid}} = this.getActiveTerm();
this.props.onContextMenu(uid, selection);
});
}
componentWillUnmount() {
this.props.ref_(null);
}
template(css) {
render() {
const shift = !isMac && this.props.termGroups.length > 1;
return (<div
className={css('terms', shift && 'termsShifted')}
>
return (
<div className={`terms_terms ${shift ? 'terms_termsShifted' : ''}`}>
{this.props.customChildrenBefore}
{
this.props.termGroups.map(termGroup => {
{this.props.termGroups.map(termGroup => {
const {uid} = termGroup;
const isActive = uid === this.props.activeRootGroup;
const props = getTermGroupProps(uid, this.props, {
@ -81,19 +88,20 @@ export default class Terms extends Component {
terms: this.terms,
activeSession: this.props.activeSession,
sessions: this.props.sessions,
customCSS: this.props.customCSS,
fontSize: this.props.fontSize,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
borderColor: this.props.borderColor,
cursorColor: this.props.cursorColor,
selectionColor: this.props.selectionColor,
colors: this.props.colors,
cursorShape: this.props.cursorShape,
cursorBlink: this.props.cursorBlink,
cursorColor: this.props.cursorColor,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
fontWeight: this.props.fontWeight,
fontWeightBold: this.props.fontWeightBold,
padding: this.props.padding,
colors: this.props.colors,
bell: this.props.bell,
bellSoundURL: this.props.bellSoundURL,
copyOnSelect: this.props.copyOnSelect,
@ -103,54 +111,56 @@ export default class Terms extends Component {
onTitle: this.props.onTitle,
onData: this.props.onData,
onURLAbort: this.props.onURLAbort,
onContextMenu: this.props.onContextMenu,
quickEdit: this.props.quickEdit,
parentProps: this.props
});
return (
<div
key={`d${uid}`}
className={css('termGroup', isActive && 'termGroupActive')}
>
<TermGroup
key={uid}
ref_={this.onRef}
{...props}
/>
<div key={`d${uid}`} className={`terms_termGroup ${isActive ? 'terms_termGroupActive' : ''}`}>
<TermGroup key={uid} ref_={this.onRef} {...props} />
</div>
);
})
}
})}
{this.props.customChildren}
</div>);
<StyleSheet
backgroundColor={this.props.backgroundColor}
customCSS={this.props.customCSS}
fontFamily={this.props.fontFamily}
foregroundColor={this.props.foregroundColor}
borderColor={this.props.borderColor}
/>
<style jsx>{`
.terms_terms {
position: absolute;
margin-top: 34px;
top: 0;
right: 0;
left: 0;
bottom: 0;
color: #fff;
transition: ${isMac ? 'none' : 'margin-top 0.3s ease'};
}
styles() {
return {
terms: {
position: 'absolute',
marginTop: '34px',
top: 0,
right: 0,
left: 0,
bottom: 0,
color: '#fff',
transition: isMac ? '' : 'margin-top 0.3s ease'
},
termsShifted: {
marginTop: '68px'
},
termGroup: {
display: 'none',
width: '100%',
height: '100%'
},
termGroupActive: {
display: 'block'
.terms_termsShifted {
margin-top: 68px;
}
};
.terms_termGroup {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: -9999em; /* Offscreen to pause xterm rendering, thanks to IntersectionObserver */
}
.terms_termGroupActive {
left: 0;
}
`}</style>
</div>
);
}
}

View file

@ -14,6 +14,9 @@ export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
export const UI_WINDOW_GEOMETRY_CHANGED = 'UI_WINDOW_GEOMETRY_CHANGED';
export const UI_OPEN_FILE = 'UI_OPEN_FILE';
export const UI_OPEN_SSH_URL = 'UI_OPEN_SSH_URL';
export const UI_OPEN_HAMBURGER_MENU = 'UI_OPEN_HAMBURGER_MENU';
export const UI_WINDOW_MINIMIZE = 'UI_WINDOW_MINIMIZE';
export const UI_WINDOW_CLOSE = 'UI_WINDOW_CLOSE';
export const UI_CONTEXTMENU_OPEN = 'UI_CONTEXTMENU_OPEN';
export const UI_COMMAND_EXEC = 'UI_COMMAND_EXEC';

View file

@ -14,7 +14,8 @@ const getActiveSessions = ({termGroups}) => termGroups.activeSessions;
const getActivityMarkers = ({ui}) => ui.activityMarkers;
const getTabs = createSelector(
[getSessions, getRootGroups, getActiveSessions, getActiveRootGroup, getActivityMarkers],
(sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) => rootGroups.map(t => {
(sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) =>
rootGroups.map(t => {
const activeSessionUid = activeSessions[t.uid];
const session = sessions[activeSessionUid];
return {

View file

@ -1,11 +1,12 @@
/* eslint-disable react/no-danger */
import Mousetrap from 'mousetrap';
import React from 'react';
import Mousetrap from 'mousetrap';
import Component from '../component';
import {connect} from '../utils/plugins';
import * as uiActions from '../actions/ui';
import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry';
import stylis from 'stylis';
import HeaderContainer from './header';
import TermsContainer from './terms';
@ -13,11 +14,16 @@ import NotificationsContainer from './notifications';
const isMac = /Mac/.test(navigator.userAgent);
class Hyper extends Component {
class Hyper extends React.PureComponent {
constructor(props) {
super(props);
this.handleFocusActive = this.handleFocusActive.bind(this);
this.handleSelectAll = this.handleSelectAll.bind(this);
this.onTermsRef = this.onTermsRef.bind(this);
this.mousetrap = null;
this.state = {
lastConfigUpdate: 0
};
}
componentWillReceiveProps(next) {
@ -26,6 +32,11 @@ class Hyper extends Component {
// starts working again
document.body.style.backgroundColor = next.backgroundColor;
}
const {lastConfigUpdate} = next;
if (lastConfigUpdate && lastConfigUpdate !== this.state.lastConfigUpdate) {
this.setState({lastConfigUpdate});
this.attachKeyListeners();
}
}
handleFocusActive() {
@ -35,44 +46,43 @@ class Hyper extends Component {
}
}
attachKeyListeners() {
const {moveTo, moveLeft, moveRight} = this.props;
handleSelectAll() {
const term = this.terms.getActiveTerm();
if (!term) {
return;
if (term) {
term.selectAll();
}
}
const lastIndex = this.terms.getLastTermIndex();
const document = term.getTermDocument();
const keys = new Mousetrap(document);
keys.bind('mod+1', moveTo.bind(this, 0));
keys.bind('mod+2', moveTo.bind(this, 1));
keys.bind('mod+3', moveTo.bind(this, 2));
keys.bind('mod+4', moveTo.bind(this, 3));
keys.bind('mod+5', moveTo.bind(this, 4));
keys.bind('mod+6', moveTo.bind(this, 5));
keys.bind('mod+7', moveTo.bind(this, 6));
keys.bind('mod+8', moveTo.bind(this, 7));
keys.bind('mod+9', moveTo.bind(this, lastIndex));
keys.bind('mod+shift+left', moveLeft);
keys.bind('mod+shift+right', moveRight);
keys.bind('mod+shift+[', moveLeft);
keys.bind('mod+shift+]', moveRight);
keys.bind('mod+alt+left', moveLeft);
keys.bind('mod+alt+right', moveRight);
keys.bind('ctrl+shift+tab', moveLeft);
keys.bind('ctrl+tab', moveRight);
attachKeyListeners() {
if (!this.mousetrap) {
this.mousetrap = new Mousetrap(window, true);
this.mousetrap.stopCallback = () => {
// All events should be intercepted even if focus is in an input/textarea
return false;
};
} else {
this.mousetrap.reset();
}
const bound = method => term[method].bind(term);
keys.bind('alt+left', bound('moveWordLeft'));
keys.bind('alt+right', bound('moveWordRight'));
keys.bind('alt+backspace', bound('deleteWordLeft'));
keys.bind('alt+del', bound('deleteWordRight'));
keys.bind('mod+backspace', bound('deleteLine'));
keys.bind('mod+left', bound('moveToStart'));
keys.bind('mod+right', bound('moveToEnd'));
keys.bind('mod+a', bound('selectAll'));
this.keys = keys;
const keys = getRegisteredKeys();
Object.keys(keys).forEach(commandKeys => {
this.mousetrap.bind(
commandKeys,
e => {
const command = keys[commandKeys];
// We should tell to xterm that it should ignore this event.
e.catched = true;
this.props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault();
},
'keydown'
);
});
}
componentDidMount() {
this.attachKeyListeners();
window.rpc.on('term selectAll', this.handleSelectAll);
}
onTermsRef(terms) {
@ -81,31 +91,23 @@ class Hyper extends Component {
componentDidUpdate(prev) {
if (prev.activeSession !== this.props.activeSession) {
if (this.keys) {
this.keys.reset();
}
this.handleFocusActive();
this.attachKeyListeners();
}
}
componentWillUnmount() {
if (this.keys) {
this.keys.reset();
}
document.body.style.backgroundColor = 'inherit';
}
template(css) {
const {isMac, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac ? '' :
`${maximized ? '0' : '1'}px`;
render() {
const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac_ ? '' : `${maximized ? '0' : '1'}px`;
return (
<div>
<div id="hyper">
<div
style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
className={css('main', isMac && 'mainRounded')}
className={`hyper_main ${isMac_ && 'hyper_mainRounded'}`}
>
<HeaderContainer />
<TermsContainer ref_={this.onTermsRef} />
@ -113,29 +115,34 @@ class Hyper extends Component {
</div>
<NotificationsContainer />
<style dangerouslySetInnerHTML={{__html: customCSS}}/>
{this.props.customChildren}
<style jsx>
{`
.hyper_main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #333;
}
.hyper_mainRounded {
border-radius: 5px;
}
`}
</style>
{/*
Add custom CSS to Hyper.
We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx
*/}
<style dangerouslySetInnerHTML={{__html: stylis('#hyper', customCSS, {prefix: false})}} />
</div>
);
}
styles() {
return {
main: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
// can be overridden by inline style above
border: '1px solid #333'
},
mainRounded: {
borderRadius: '5px'
}
};
}
}
const HyperContainer = connect(
@ -147,21 +154,14 @@ const HyperContainer = connect(
borderColor: state.ui.borderColor,
activeSession: state.sessions.activeUid,
backgroundColor: state.ui.backgroundColor,
maximized: state.ui.maximized
maximized: state.ui.maximized,
lastConfigUpdate: state.ui._lastUpdate
};
},
dispatch => {
return {
moveTo: i => {
dispatch(uiActions.moveTo(i));
},
moveLeft: () => {
dispatch(uiActions.moveLeft());
},
moveRight: () => {
dispatch(uiActions.moveRight());
execCommand: (command, fn, e) => {
dispatch(uiActions.execCommand(command, fn, e));
}
};
},

View file

@ -34,7 +34,9 @@ const NotificationsContainer = connect(
Object.assign(state_, {
updateShowing: true,
updateVersion: ui.updateVersion,
updateNote: ui.updateNotes.split('\n')[0]
updateNote: ui.updateNotes.split('\n')[0],
updateReleaseUrl: ui.updateReleaseUrl,
updateCanInstall: ui.updateCanInstall
});
} else if (notifications.message) {
Object.assign(state_, {

View file

@ -7,6 +7,7 @@ import {
setSessionXtermTitle,
setActiveSession
} from '../actions/sessions';
import {openContextMenu} from '../actions/ui';
import getRootGroups from '../selectors';
const TermsContainer = connect(
@ -21,17 +22,19 @@ const TermsContainer = connect(
activeSession: state.sessions.activeUid,
customCSS: state.ui.termCSS,
write: state.sessions.write,
fontSize: state.ui.fontSizeOverride ?
state.ui.fontSizeOverride :
state.ui.fontSize,
fontSize: state.ui.fontSizeOverride ? state.ui.fontSizeOverride : state.ui.fontSize,
fontFamily: state.ui.fontFamily,
fontWeight: state.ui.fontWeight,
fontWeightBold: state.ui.fontWeightBold,
uiFontFamily: state.ui.uiFontFamily,
fontSmoothing: state.ui.fontSmoothingOverride,
padding: state.ui.padding,
cursorColor: state.ui.cursorColor,
cursorAccentColor: state.ui.cursorAccentColor,
cursorShape: state.ui.cursorShape,
cursorBlink: state.ui.cursorBlink,
borderColor: state.ui.borderColor,
selectionColor: state.ui.selectionColor,
colors: state.ui.colors,
foregroundColor: state.ui.foregroundColor,
backgroundColor: state.ui.backgroundColor,
@ -49,8 +52,7 @@ const TermsContainer = connect(
},
onTitle(uid, title) {
// we need to trim the title because `cmd.exe` likes to report ' ' as the title
dispatch(setSessionXtermTitle(uid, title.trim()));
dispatch(setSessionXtermTitle(uid, title));
},
onResize(uid, cols, rows) {
@ -63,6 +65,11 @@ const TermsContainer = connect(
onActive(uid) {
dispatch(setActiveSession(uid));
},
onContextMenu(uid, selection) {
dispatch(setActiveSession(uid));
dispatch(openContextMenu(uid, selection));
}
};
},

View file

@ -1,492 +0,0 @@
import {clipboard} from 'electron';
import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';
import fromCharCode from './utils/key-code';
import selection from './utils/selection';
import returnKey from './utils/keymaps';
import CommandRegistry from './command-registry';
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
const cache = [];
lib.wc.strWidth = function (str) {
const shouldCache = str.length === 1;
if (shouldCache && cache[str] !== undefined) {
return cache[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);
}
if (shouldCache) {
cache[str] = rv;
}
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;
vertical-align: top;
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);
}
};
let lastEventTimeStamp;
let lastEventKey;
// 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.timeStamp === lastEventTimeStamp && e.key === lastEventKey) {
// Event was already processed.
// It seems to occur after a char composition ended by Tab and cause a blur.
// See https://github.com/zeit/hyper/issues/1341
e.preventDefault();
return;
}
lastEventTimeStamp = e.timeStamp;
lastEventKey = e.key;
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();
}
// test key from keymaps before moving forward with actions
const key = returnKey(e);
if (key) {
if (CommandRegistry.getCommand(key)) {
CommandRegistry.exec(key, e);
}
}
if (e.altKey || e.metaKey || key) {
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (e.key === 'v' && this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();
}
return;
}
// Test for valid keys in order to accept clear status
const clearBlacklist = [
'control',
'shift',
'capslock',
'dead'
];
if (!clearBlacklist.includes(e.code.toLowerCase()) &&
!clearBlacklist.includes(e.key.toLowerCase())) {
// Since Electron 1.6.X, there is a race condition with character composition
// if this selection clearing is made synchronously. See #2140.
setTimeout(() => selection.clear(this.terminal), 0);
}
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();
}
return oldKeyDown.call(this, e);
};
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;
}
return oldOnMouse.call(this, e);
};
hterm.Terminal.prototype.onMouseDown_ = function (e) {
// copy/paste on right click
if (e.button === 2) {
const text = this.getSelectionText();
if (text) {
this.copyStringToClipboard(text);
} else {
this.onVTKeystroke(clipboard.readText());
}
}
};
// override `ScrollPort.resize` to avoid an expensive calculation
// just to get the size of the scrollbar, which for Hyper is always
// set to overlay (hence with `0`)
hterm.ScrollPort.prototype.resize = function () {
this.currentScrollbarWidthPx = 0;
this.syncScrollHeight();
this.syncRowNodesDimensions_();
this.publish(
'resize',
{scrollPort: this},
() => {
this.scrollRowToBottom(this.rowProvider_.getRowCount());
this.scheduleRedraw();
}
);
};
// 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);
};
// For some reason, when the original version of this function was called right
// after a new tab was created, it was breaking the focus of the other tab.
// After some investigation, I (matheuss) found that `this.iframe_.focus();` (from
// the original function) was causing the issue. So right now we're overriding
// the function to prevent the `iframe_` from being focused.
// This shouldn't create any side effects we're _stealing_ the focus from `htem` anyways.
hterm.ScrollPort.prototype.focus = function () {
this.screen_.focus();
};
// 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 (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = convert(arg[i]);
}
} else {
arg = convert(arg);
}
return arg;
};
// add support for cursor styles 5 and 6, fixes #270
hterm.VT.CSI[' q'] = function (parseState) {
const arg = parseState.args[0];
if (arg === '0' || arg === '1') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(true);
} else if (arg === '2') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(false);
} else if (arg === '3') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(true);
} else if (arg === '4') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(false);
} else if (arg === '5') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(true);
} else if (arg === '6') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(false);
} else {
console.warn('Unknown cursor style: ' + arg);
}
};
export default hterm;
export {lib};

View file

@ -44,30 +44,11 @@ rpc.on('session add', data => {
store_.dispatch(sessionActions.addSession(data));
});
// we aggregate all the incoming pty events by raf
// debouncing, to reduce allocation and iterations
let req;
let objects = {};
rpc.on('session data', d => {
// the uid is a uuid v4 so it's 36 chars long
const uid = d.slice(0, 36);
const data = d.slice(36);
if (objects[uid] === undefined) {
objects[uid] = data;
} else {
objects[uid] += data;
}
if (!req) {
req = requestAnimationFrame(() => {
for (const i in objects) {
if ({}.hasOwnProperty.call(objects, i)) {
store_.dispatch(sessionActions.addSessionData(i, objects[i]));
}
}
objects = {};
req = null;
});
}
store_.dispatch(sessionActions.addSessionData(uid, data));
});
rpc.on('session data send', ({uid, data, escaped}) => {
@ -86,6 +67,42 @@ rpc.on('session clear req', () => {
store_.dispatch(sessionActions.clearActiveSession());
});
rpc.on('session move word left req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bb'));
});
rpc.on('session move word right req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bf'));
});
rpc.on('session move line beginning req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bOH'));
});
rpc.on('session move line end req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bOF'));
});
rpc.on('session del word left req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1b\x7f'));
});
rpc.on('session del word right req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bd'));
});
rpc.on('session del line beginning req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bw'));
});
rpc.on('session del line end req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x10B'));
});
rpc.on('session break req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x03'));
});
rpc.on('termgroup add req', () => {
store_.dispatch(termGroupActions.requestTermGroup());
});
@ -118,6 +135,10 @@ rpc.on('move right req', () => {
store_.dispatch(uiActions.moveRight());
});
rpc.on('move jump req', index => {
store_.dispatch(uiActions.moveTo(index));
});
rpc.on('next pane req', () => {
store_.dispatch(uiActions.moveToNextPane());
});
@ -130,8 +151,12 @@ rpc.on('open file', ({path}) => {
store_.dispatch(uiActions.openFile(path));
});
rpc.on('update available', ({releaseName, releaseNotes}) => {
store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes));
rpc.on('open ssh', url => {
store_.dispatch(uiActions.openSSH(url));
});
rpc.on('update available', ({releaseName, releaseNotes, releaseUrl, canInstall}) => {
store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes, releaseUrl, canInstall));
});
rpc.on('move', () => {

View file

@ -35,15 +35,16 @@ function Session(obj) {
const reducer = (state = initialState, action) => {
switch (action.type) {
case SESSION_ADD:
return state
.set('activeUid', action.uid)
.setIn(['sessions', action.uid], Session({
return state.set('activeUid', action.uid).setIn(
['sessions', action.uid],
Session({
cols: action.cols,
rows: action.rows,
uid: action.uid,
shell: action.shell.split('/').pop(),
pid: action.pid
}));
})
);
case SESSION_URL_SET:
return state.setIn(['sessions', action.uid, 'url'], action.url);
@ -55,27 +56,31 @@ const reducer = (state = initialState, action) => {
return state.set('activeUid', action.uid);
case SESSION_CLEAR_ACTIVE:
return state.merge({
return state.merge(
{
sessions: {
[state.activeUid]: {
cleared: true
}
}
}, {deep: true});
},
{deep: true}
);
case SESSION_PTY_DATA:
// we avoid a direct merge for perf reasons
// as this is the most common action
if (state.sessions[action.uid] &&
state.sessions[action.uid].cleared) {
return state
.merge({
if (state.sessions[action.uid] && state.sessions[action.uid].cleared) {
return state.merge(
{
sessions: {
[action.uid]: {
cleared: false
}
}
}, {deep: true});
},
{deep: true}
);
}
return state;
@ -83,6 +88,7 @@ const reducer = (state = initialState, action) => {
if (state.sessions[action.uid]) {
return deleteSession(state, action.uid);
}
// eslint-disable-next-line no-console
console.log('ignore pty exit: session removed by user');
return state;
@ -90,14 +96,22 @@ const reducer = (state = initialState, action) => {
return deleteSession(state, action.uid);
case SESSION_SET_XTERM_TITLE:
return state.setIn(['sessions', action.uid, 'title'], action.title);
return state.setIn(
['sessions', action.uid, 'title'],
// we need to trim the title because `cmd.exe`
// likes to report ' ' as the title
action.title.trim()
);
case SESSION_RESIZE:
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({
return state.setIn(
['sessions', action.uid],
state.sessions[action.uid].merge({
rows: action.rows,
cols: action.cols,
resizeAt: action.now
}));
})
);
case SESSION_SET_CWD:
if (state.activeUid) {

View file

@ -40,9 +40,7 @@ const setActiveGroup = (state, action) => {
const childGroup = findBySession(state, action.uid);
const rootGroup = findRootGroup(state.termGroups, childGroup.uid);
return state
.set('activeRootGroup', rootGroup.uid)
.setIn(['activeSessions', rootGroup.uid], action.uid);
return state.set('activeRootGroup', rootGroup.uid).setIn(['activeSessions', rootGroup.uid], action.uid);
};
// Reduce existing sizes to fit a new split:
@ -50,7 +48,7 @@ const insertRebalance = (oldSizes, index) => {
const newSize = 1 / (oldSizes.length + 1);
// We spread out how much each pane should be reduced
// with based on their existing size:
const balanced = oldSizes.map(size => size - (newSize * size));
const balanced = oldSizes.map(size => size - newSize * size);
return [...balanced.slice(0, index), newSize, ...balanced.slice(index)];
};
@ -58,9 +56,7 @@ const insertRebalance = (oldSizes, index) => {
const removalRebalance = (oldSizes, index) => {
const removedSize = oldSizes[index];
const increase = removedSize / (oldSizes.length - 1);
return oldSizes
.filter((_size, i) => i !== index)
.map(size => size + increase);
return oldSizes.filter((_size, i) => i !== index).map(size => size + increase);
};
const splitGroup = (state, action) => {
@ -96,23 +92,27 @@ const splitGroup = (state, action) => {
parentUid: parentGroup.uid
});
return state
.setIn(['termGroups', existingSession.uid], existingSession)
.setIn(['termGroups', parentGroup.uid], parentGroup.merge({
return state.setIn(['termGroups', existingSession.uid], existingSession).setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
sessionUid: null,
direction: splitDirection,
children: [existingSession.uid, newSession.uid]
}));
})
);
}
const {children} = parentGroup;
// Insert the new child pane right after the active one:
const index = children.indexOf(activeGroup.uid) + 1;
const newChildren = [...children.slice(0, index), newSession.uid, ...children.slice(index)];
state = state.setIn(['termGroups', parentGroup.uid], parentGroup.merge({
state = state.setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
direction: splitDirection,
children: newChildren
}));
})
);
if (parentGroup.sizes) {
const newSizes = insertRebalance(parentGroup.sizes, index);
@ -131,17 +131,13 @@ const replaceParent = (state, parent, child) => {
// If the parent we're replacing has a parent,
// we need to change the uid in its children array
// with `child`:
const newChildren = parentParent.children.map(uid =>
uid === parent.uid ? child.uid : uid
);
const newChildren = parentParent.children.map(uid => (uid === parent.uid ? child.uid : uid));
state = state.setIn(['termGroups', parentParent.uid, 'children'], newChildren);
} else {
// This means the given child will be
// a root group, so we need to set it up as such:
const newSessions = state.activeSessions
.without(parent.uid)
.set(child.uid, state.activeSessions[parent.uid]);
const newSessions = state.activeSessions.without(parent.uid).set(child.uid, state.activeSessions[parent.uid]);
state = state
.set('activeTermGroup', child.uid)
@ -185,10 +181,7 @@ const resizeGroup = (state, uid, sizes) => {
return state;
}
return state.setIn(
['termGroups', uid, 'sizes'],
sizes
);
return state.setIn(['termGroups', uid, 'sizes'], sizes);
};
const reducer = (state = initialState, action) => {

View file

@ -20,7 +20,6 @@ import {
SESSION_SET_CWD
} from '../constants/sessions';
import {UPDATE_AVAILABLE} from '../constants/updater';
import {values} from '../utils/object';
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
const allowedCursorBlinkValues = new Set([true, false]);
@ -34,36 +33,41 @@ const initial = Immutable({
rows: null,
activeUid: null,
cursorColor: '#F81CE5',
cursorAccentColor: '#000',
cursorShape: 'BLOCK',
cursorBlink: false,
borderColor: '#333',
selectionColor: 'rgba(248,28,229,0.3)',
fontSize: 12,
padding: '12px 14px',
fontFamily: 'Menlo, "DejaVu Sans Mono", "Lucida Console", monospace',
uiFontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
uiFontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSizeOverride: null,
fontSmoothingOverride: 'antialiased',
fontWeight: 'normal',
fontWeightBold: 'bold',
css: '',
termCSS: '',
openAt: {},
resizeAt: 0,
colors: {
black: '#000000',
red: '#ff0000',
green: '#33ff00',
yellow: '#ffff00',
blue: '#0066ff',
magenta: '#cc00ff',
cyan: '#00ffff',
white: '#d0d0d0',
lightBlack: '#808080',
lightRed: '#ff0000',
lightGreen: '#33ff00',
lightYellow: '#ffff00',
lightBlue: '#0066ff',
lightMagenta: '#cc00ff',
lightCyan: '#00ffff',
lightWhite: '#ffffff'
red: '#C51E14',
green: '#1DC121',
yellow: '#C7C329',
blue: '#0A2FC4',
magenta: '#C839C5',
cyan: '#20C5C6',
white: '#C7C7C7',
lightBlack: '#686868',
lightRed: '#FD6F6B',
lightGreen: '#67F86F',
lightYellow: '#FFFA72',
lightBlue: '#6A76FB',
lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE',
lightWhite: '#FFFFFF'
},
activityMarkers: {},
notifications: {
@ -97,15 +101,17 @@ const currentWindow = remote.getCurrentWindow();
const reducer = (state = initial, action) => {
let state_ = state;
let isMax;
switch (action.type) { // eslint-disable-line default-case
//eslint-disable-next-line default-case
switch (action.type) {
case CONFIG_LOAD:
case CONFIG_RELOAD: // eslint-disable-line no-case-declarations
const {config} = action;
// eslint-disable-next-line no-case-declarations, no-fallthrough
case CONFIG_RELOAD:
const {config, now} = action;
state_ = state
// unset the user font size override if the
// font size changed from the config
.merge((() => {
.merge(
(() => {
const ret = {};
if (state.fontSizeOverride && config.fontSize !== state.fontSize) {
@ -124,10 +130,26 @@ const reducer = (state = initial, action) => {
ret.uiFontFamily = config.uiFontFamily;
}
if (config.fontWeight) {
ret.fontWeight = config.fontWeight;
}
if (config.fontWeightBold) {
ret.fontWeightBold = config.fontWeightBold;
}
if (config.uiFontFamily) {
ret.uiFontFamily = config.uiFontFamily;
}
if (config.cursorColor) {
ret.cursorColor = config.cursorColor;
}
if (config.cursorAccentColor) {
ret.cursorAccentColor = config.cursorAccentColor;
}
if (allowedCursorShapes.has(config.cursorShape)) {
ret.cursorShape = config.cursorShape;
}
@ -140,8 +162,11 @@ const reducer = (state = initial, action) => {
ret.borderColor = config.borderColor;
}
if (typeof (config.padding) !== 'undefined' &&
config.padding !== null) {
if (config.selectionColor) {
ret.selectionColor = config.selectionColor;
}
if (typeof config.padding !== 'undefined' && config.padding !== null) {
ret.padding = config.padding;
}
@ -153,7 +178,7 @@ const reducer = (state = initial, action) => {
ret.backgroundColor = config.backgroundColor;
}
if (config.css) {
if (config.css || config.css === '') {
ret.css = config.css;
}
@ -169,21 +194,12 @@ const reducer = (state = initial, action) => {
ret.bellSoundURL = config.bellSoundURL || initial.bellSoundURL;
}
if (typeof (config.copyOnSelect) !== 'undefined' &&
config.copyOnSelect !== null) {
if (typeof config.copyOnSelect !== 'undefined' && config.copyOnSelect !== null) {
ret.copyOnSelect = config.copyOnSelect;
}
if (config.colors) {
if (Array.isArray(config.colors)) {
const stateColors = Array.isArray(state.colors) ?
state.colors :
values(state.colors);
if (stateColors.toString() !== config.colors.toString()) {
ret.colors = config.colors;
}
} else if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) {
if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) {
ret.colors = config.colors;
}
}
@ -200,25 +216,29 @@ const reducer = (state = initial, action) => {
ret.showWindowControls = config.showWindowControls;
}
if (process.platform === 'win32' &&
(config.quickEdit === undefined || config.quickEdit === null)) {
if (process.platform === 'win32' && (config.quickEdit === undefined || config.quickEdit === null)) {
ret.quickEdit = true;
} else if (typeof (config.quickEdit) !== 'undefined' &&
config.quickEdit !== null) {
} else if (typeof config.quickEdit !== 'undefined' && config.quickEdit !== null) {
ret.quickEdit = config.quickEdit;
}
ret._lastUpdate = now;
return ret;
})());
})()
);
break;
case SESSION_ADD:
state_ = state.merge({
state_ = state.merge(
{
activeUid: action.uid,
openAt: {
[action.uid]: action.now
}
}, {deep: true});
},
{deep: true}
);
break;
case SESSION_RESIZE:
@ -250,15 +270,19 @@ const reducer = (state = initial, action) => {
break;
case SESSION_SET_ACTIVE:
state_ = state.merge({
state_ = state.merge(
{
activeUid: action.uid,
activityMarkers: {
[action.uid]: false
}
}, {deep: true});
},
{deep: true}
);
break;
case SESSION_PTY_DATA: // eslint-disable-line no-case-declarations
// eslint-disable-next-line no-case-declarations
case SESSION_PTY_DATA:
// ignore activity markers for current tab
if (action.uid === state.activeUid) {
break;
@ -274,11 +298,14 @@ const reducer = (state = initial, action) => {
// expect to get data packets from the resize
// of the ptys as a result
if (!state.resizeAt || action.now - state.resizeAt > 1000) {
state_ = state.merge({
state_ = state.merge(
{
activityMarkers: {
[action.uid]: true
}
}, {deep: true});
},
{deep: true}
);
}
break;
@ -315,11 +342,14 @@ const reducer = (state = initial, action) => {
break;
case NOTIFICATION_DISMISS:
state_ = state.merge({
state_ = state.merge(
{
notifications: {
[action.id]: false
}
}, {deep: true});
},
{deep: true}
);
break;
case NOTIFICATION_MESSAGE:
@ -333,22 +363,26 @@ const reducer = (state = initial, action) => {
case UPDATE_AVAILABLE:
state_ = state.merge({
updateVersion: action.version,
updateNotes: action.notes || ''
updateNotes: action.notes || '',
updateReleaseUrl: action.releaseUrl,
updateCanInstall: !!action.canInstall
});
break;
}
// Show a notification if any of the font size values have changed
if (CONFIG_LOAD !== action.type) {
if (state_.fontSize !== state.fontSize ||
state_.fontSizeOverride !== state.fontSizeOverride) {
if (state_.fontSize !== state.fontSize || state_.fontSizeOverride !== state.fontSizeOverride) {
state_ = state_.merge({notifications: {font: true}}, {deep: true});
}
}
if ((typeof (state.cols) !== 'undefined' && state.cols !== null) &&
(typeof (state.rows) !== 'undefined' && state.rows !== null) &&
(state.rows !== state_.rows || state.cols !== state_.cols)) {
if (
typeof state.cols !== 'undefined' &&
state.cols !== null &&
(typeof state.rows !== 'undefined' && state.rows !== null) &&
(state.rows !== state_.rows || state.cols !== state_.cols)
) {
state_ = state_.merge({notifications: {resize: true}}, {deep: true});
}

View file

@ -1,9 +1,8 @@
import {createSelector} from 'reselect';
const getTermGroups = ({termGroups}) => termGroups.termGroups;
const getRootGroups = createSelector(
getTermGroups,
termGroups => Object.keys(termGroups)
const getRootGroups = createSelector(getTermGroups, termGroups =>
Object.keys(termGroups)
.map(uid => termGroups[uid])
.filter(({parentUid}) => !parentUid)
);

View file

@ -1,31 +1,15 @@
import {createStore, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import {createLogger} from 'redux-logger';
import rootReducer from '../reducers/index';
import effects from '../utils/effects';
import * as plugins from '../utils/plugins';
import writeMiddleware from './write-middleware';
export default () => {
const logger = createLogger({
level: 'info',
collapsed: true
});
const enhancer = compose(
applyMiddleware(
thunk,
plugins.middleware,
thunk,
effects,
writeMiddleware,
logger
),
applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects),
window.devToolsExtension()
);
return createStore(
rootReducer,
enhancer
);
return createStore(rootReducer, enhancer);
};

View file

@ -6,13 +6,4 @@ import * as plugins from '../utils/plugins';
import writeMiddleware from './write-middleware';
export default () =>
createStore(
rootReducer,
applyMiddleware(
thunk,
plugins.middleware,
thunk,
effects,
writeMiddleware
)
);
createStore(rootReducer, applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects));

View file

@ -7,7 +7,7 @@ export default () => next => action => {
if (action.type === 'SESSION_PTY_DATA') {
const term = terms[action.uid];
if (term) {
term.write(action.data);
term.term.write(action.data);
}
}
next(action);

View file

@ -15,9 +15,5 @@ export default function isExecutable(fileStat) {
return true;
}
return Boolean(
(fileStat.mode & 0o0001) ||
(fileStat.mode & 0o0010) ||
(fileStat.mode & 0o0100)
);
return Boolean(fileStat.mode & 0o0001 || fileStat.mode & 0o0010 || fileStat.mode & 0o0100);
}

Some files were not shown because too many files have changed in this diff Show more