mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
Add Windows support and first-class Linux support (#946)
* `child_pty` => `pty.js` * Create a frameless window on Windows and Linux * Add a brand new UI for Linux and Windows 💅 * [Windows] Fix plugin installation * [Windows] Fix the `build` script * [Windows] Add a bigger `icon.ico` * [Mac] Add `WebKitAppRegion: drag` when running on macOS * Fix code style 🤔 * Add `appveyor.yml` * Fix code style (again) * [Windows] Fix AppVeyor's `install` script * [Windows] Try a new AppVeyor config * [Windows] Set the binary path so Spectron can run the tests * [Windows] Try to build on x64 * Try again to build on x64 * Try one more time 😩 * Throw an error to indicate that `pty.js` was built incorrectly * [Win/Linux] Add `display: hidden` to <Tabs /> if tabs.length === 1 * [Win/Linux] Reorganize SVGs – via @CodeTheory * [Win/Linux] Fix the hamburger menu height * Make the SVGs look better with `shape-rendering: crispEdges;` * [Win/Linux] Add config options for the window controls and the 🍔 menu * Add `electron-squirrel-startup` dependency * [Win] Handle Squirrel commands * [Win/Linux] Fix default color for the 🍔 and window controls – via @CodeTheory * [Win/Linux] Add some padding - via @CodeTheory * [Win/Linux] Add hover states – via @CodeTheory * [Win] Fix empty window/tab titles * [Win] Fix opening Preferences (#978) * [Win] Fix opening Preferences * Update ui.js * Update ui.js * Enhance messages and default editor * [Win] Add dependency instructions to the README.md [skip ci] * Fix code style * [Win/Linux] Check the number of open windows before quitting the app
This commit is contained in:
parent
26701d43b5
commit
9c90e19760
21 changed files with 433 additions and 74 deletions
|
|
@ -20,7 +20,9 @@ $ brew cask install hyper
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
1. If you are running Linux, install "icnsutils", "graphicsmagick" and "xz-utils"
|
1. Install the dependencies
|
||||||
|
* If you are running Linux, install `icnsutils`, `graphicsmagick` and `xz-utils`
|
||||||
|
* If you are running Windows, install [VC++ Build Tools Technical Preview](http://go.microsoft.com/fwlink/?LinkId=691126) using the **Default Install option**; Install Python 2.7 and add it to your `%PATH%`; Run `npm config set msvs_version 2015 --global`
|
||||||
2. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
|
2. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
|
||||||
3. Install the dependencies: `npm install`
|
3. Install the dependencies: `npm install`
|
||||||
4. Build the code, watch for changes and run the app: `npm start`
|
4. Build the code, watch for changes and run the app: `npm start`
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,16 @@ module.exports = {
|
||||||
// custom css to embed in the terminal window
|
// custom css to embed in the terminal window
|
||||||
termCSS: '',
|
termCSS: '',
|
||||||
|
|
||||||
|
// set to `true` if you're using a Linux set up
|
||||||
|
// that doesn't shows native menus
|
||||||
|
// default: `false` on Linux, `true` on Windows (ignored on macOS)
|
||||||
|
showHamburgerMenu: '',
|
||||||
|
|
||||||
|
// set to `false` 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)
|
||||||
|
showWindowControls: '',
|
||||||
|
|
||||||
// custom padding (css format, i.e.: `top right bottom left`)
|
// custom padding (css format, i.e.: `top right bottom left`)
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
|
|
||||||
|
|
|
||||||
20
app/index.js
20
app/index.js
|
|
@ -1,3 +1,5 @@
|
||||||
|
// eslint-disable-next-line curly, unicorn/no-process-exit
|
||||||
|
if (require('electron-squirrel-startup')) process.exit();
|
||||||
// Native
|
// Native
|
||||||
const {resolve} = require('path');
|
const {resolve} = require('path');
|
||||||
|
|
||||||
|
|
@ -108,9 +110,11 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
height,
|
height,
|
||||||
minHeight: 190,
|
minHeight: 190,
|
||||||
minWidth: 370,
|
minWidth: 370,
|
||||||
titleBarStyle: 'hidden-inset',
|
titleBarStyle: 'hidden-inset', // macOS only
|
||||||
title: 'Hyper.app',
|
title: 'Hyper.app',
|
||||||
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
|
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
|
||||||
|
// we want to go frameless on windows and linux
|
||||||
|
frame: process.platform === 'darwin',
|
||||||
transparent: true,
|
transparent: true,
|
||||||
icon: resolve(__dirname, 'static/icon.png'),
|
icon: resolve(__dirname, 'static/icon.png'),
|
||||||
// we only want to show when the prompt is ready for user input
|
// we only want to show when the prompt is ready for user input
|
||||||
|
|
@ -235,6 +239,18 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
rpc.emit('move');
|
rpc.emit('move');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rpc.on('open hamburger menu', ({x, y}) => {
|
||||||
|
Menu.getApplicationMenu().popup(x, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('minimize', () => {
|
||||||
|
win.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('close', () => {
|
||||||
|
win.close();
|
||||||
|
});
|
||||||
|
|
||||||
const deleteSessions = () => {
|
const deleteSessions = () => {
|
||||||
sessions.forEach((session, key) => {
|
sessions.forEach((session, key) => {
|
||||||
session.removeAllListeners();
|
session.removeAllListeners();
|
||||||
|
|
@ -305,7 +321,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin' && windowSet.size === 0) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,19 @@
|
||||||
"repository": "zeit/hyper",
|
"repository": "zeit/hyper",
|
||||||
"xo": false,
|
"xo": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"child_pty": "3.0.1",
|
|
||||||
"color": "0.11.3",
|
"color": "0.11.3",
|
||||||
"convert-css-color-name-to-hex": "0.1.1",
|
"convert-css-color-name-to-hex": "0.1.1",
|
||||||
"default-shell": "1.0.1",
|
"default-shell": "1.0.1",
|
||||||
"electron-config": "0.2.1",
|
"electron-config": "0.2.1",
|
||||||
"electron-is-dev": "0.1.1",
|
"electron-is-dev": "0.1.1",
|
||||||
|
"electron-squirrel-startup": "1.0.0",
|
||||||
"file-uri-to-path": "0.0.2",
|
"file-uri-to-path": "0.0.2",
|
||||||
"gaze": "1.1.2",
|
"gaze": "1.1.2",
|
||||||
"git-describe": "3.0.2",
|
"git-describe": "3.0.2",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"ms": "0.7.1",
|
"ms": "0.7.1",
|
||||||
"node-fetch": "1.6.3",
|
"node-fetch": "1.6.3",
|
||||||
|
"pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642",
|
||||||
"semver": "5.3.0",
|
"semver": "5.3.0",
|
||||||
"shell-env": "0.2.0",
|
"shell-env": "0.2.0",
|
||||||
"uuid": "2.0.2"
|
"uuid": "2.0.2"
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ function install(fn) {
|
||||||
env.npm_config_target = '1.3.0';
|
env.npm_config_target = '1.3.0';
|
||||||
env.npm_config_disturl = 'https://atom.io/download/atom-shell';
|
env.npm_config_disturl = 'https://atom.io/download/atom-shell';
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
exec('npm prune; npm install --production', {
|
exec('npm prune && npm install --production', {
|
||||||
cwd: path,
|
cwd: path,
|
||||||
env,
|
env,
|
||||||
shell
|
shell
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,13 @@ const {getDecoratedEnv} = require('./plugins');
|
||||||
const {productName, version} = require('./package');
|
const {productName, version} = require('./package');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
|
const createPtyJsError = () => new Error('`pty.js` failed to load. Typically this means that it was built incorrectly. Please check the `README.me` to more info.');
|
||||||
|
|
||||||
let spawn;
|
let spawn;
|
||||||
try {
|
try {
|
||||||
spawn = require('child_pty').spawn;
|
spawn = require('pty.js').spawn;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
throw createPtyJsError();
|
||||||
'A native module failed to load. Typically this means ' +
|
|
||||||
'you installed the modules incorrectly.\n Use `scripts/install.sh` ' +
|
|
||||||
'to trigger the installation.\n ' +
|
|
||||||
'More information: https://github.com/zeit/hyper/issues/72'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFromConfig = config.getConfig().env || {};
|
const envFromConfig = config.getConfig().env || {};
|
||||||
|
|
@ -37,12 +34,20 @@ module.exports = class Session extends EventEmitter {
|
||||||
|
|
||||||
const defaultShellArgs = ['--login'];
|
const defaultShellArgs = ['--login'];
|
||||||
|
|
||||||
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, {
|
try {
|
||||||
columns,
|
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, {
|
||||||
rows,
|
columns,
|
||||||
cwd,
|
rows,
|
||||||
env: getDecoratedEnv(baseEnv)
|
cwd,
|
||||||
});
|
env: getDecoratedEnv(baseEnv)
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (/is not a function/.test(err.message)) {
|
||||||
|
throw createPtyJsError();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.pty.stdout.on('data', data => {
|
this.pty.stdout.on('data', data => {
|
||||||
if (this.ended) {
|
if (this.ended) {
|
||||||
|
|
@ -69,9 +74,9 @@ module.exports = class Session extends EventEmitter {
|
||||||
this.pty.stdin.write(data);
|
this.pty.stdin.write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({cols: columns, rows}) {
|
resize({cols, rows}) {
|
||||||
try {
|
try {
|
||||||
this.pty.stdout.resize({columns, rows});
|
this.pty.stdout.resize(cols, rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +84,7 @@ module.exports = class Session extends EventEmitter {
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
try {
|
try {
|
||||||
this.pty.kill('SIGHUP');
|
this.pty.kill();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('exit error', err.stack);
|
console.error('exit error', err.stack);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
appveyor.yml
Normal file
31
appveyor.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# https://github.com/sindresorhus/appveyor-node/blob/master/appveyor.yml
|
||||||
|
|
||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
- platform: x64
|
||||||
|
|
||||||
|
image: Visual Studio 2015
|
||||||
|
|
||||||
|
init:
|
||||||
|
- npm config set msvs_version 2015 # we need this to build `pty.js`
|
||||||
|
|
||||||
|
cache:
|
||||||
|
- node_modules
|
||||||
|
|
||||||
|
install:
|
||||||
|
- ps: Install-Product node 6 x64
|
||||||
|
- set CI=true
|
||||||
|
- npm -g install npm@latest
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
build: off
|
||||||
|
|
||||||
|
shallow_clone: true
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- node --version
|
||||||
|
- npm --version
|
||||||
|
- npm run test
|
||||||
|
|
||||||
|
on_success:
|
||||||
|
- npm run dist
|
||||||
|
|
@ -1,8 +1,48 @@
|
||||||
<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<defs>
|
<defs>
|
||||||
<symbol id="close" viewBox="0 0 24 24">
|
<symbol id="close-tab" viewBox="0 0 24 24">
|
||||||
<title>close</title>
|
<title>close tab</title>
|
||||||
<g><path d='M13.1919001,11.9997324 L23.7528721,22.5607045 C24.0822321,22.8904941 24.0822321,23.4241533 23.7528721,23.7526581 C23.4235121,24.0824473 22.8898521,24.0824473 22.5609201,23.7526581 L11.999948,13.1916857 L1.43897601,23.7526581 C1.109612,24.0824473 0.575952002,24.0824473 0.247020001,23.7526581 C-0.0823400003,23.4237253 -0.0823400003,22.8900657 0.247020001,22.5607045 L10.80842,11.9997324 L0.247020001,1.43961681 C-0.0823400003,1.110684 -0.0823400003,0.576168002 0.247020001,0.247663201 C0.576384002,-0.0825544003 1.11004,-0.0825544003 1.43897601,0.247663201 L11.999948,10.8082072 L22.5609201,0.247663201 C22.8902801,-0.0825544003 23.4239401,-0.0825544003 23.7528721,0.247663201 C24.0822321,0.576596002 24.0822321,1.111112 23.7528721,1.43961681 L13.1919001,11.9997324 L13.1919001,11.9997324 L13.1919001,11.9997324 Z'></path></g>
|
<g><path d='M13.1919001,11.9997324 L23.7528721,22.5607045 C24.0822321,22.8904941 24.0822321,23.4241533 23.7528721,23.7526581 C23.4235121,24.0824473 22.8898521,24.0824473 22.5609201,23.7526581 L11.999948,13.1916857 L1.43897601,23.7526581 C1.109612,24.0824473 0.575952002,24.0824473 0.247020001,23.7526581 C-0.0823400003,23.4237253 -0.0823400003,22.8900657 0.247020001,22.5607045 L10.80842,11.9997324 L0.247020001,1.43961681 C-0.0823400003,1.110684 -0.0823400003,0.576168002 0.247020001,0.247663201 C0.576384002,-0.0825544003 1.11004,-0.0825544003 1.43897601,0.247663201 L11.999948,10.8082072 L22.5609201,0.247663201 C22.8902801,-0.0825544003 23.4239401,-0.0825544003 23.7528721,0.247663201 C24.0822321,0.576596002 24.0822321,1.111112 23.7528721,1.43961681 L13.1919001,11.9997324 L13.1919001,11.9997324 L13.1919001,11.9997324 Z'></path></g>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="hamburger-menu" viewBox="0 0 10 10">
|
||||||
|
<title>hamburger menu</title>
|
||||||
|
<rect y="0.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
<rect y="4.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
<rect y="8.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="minimize-window" viewBox="0 0 10 10">
|
||||||
|
<title>minimize window</title>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<path stroke="currentColor" d="M9.5,5 L0.5,5" stroke-linecap="square"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<rect width="10" height="1" y="4.5" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="maximize-window" viewBox="0 0 10 10">
|
||||||
|
<title>maximize window</title>
|
||||||
|
<defs>
|
||||||
|
<polygon id="maximize-window-a" points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<mask id="maximize-window-b" width="10" height="10" x="0" y="0">
|
||||||
|
<use xlink:href="#maximize-window-a"/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon fill="none" points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<use stroke="currentColor" stroke-width="2" mask="url(#maximize-window-b)" xlink:href="#maximize-window-a"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="close-window" viewBox="0 0 10 10">
|
||||||
|
<title>close window</title>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g stroke="currentColor" transform="translate(.25 .25)" stroke-linecap="square">
|
||||||
|
<path d="M0.5,0.5 L9,9"/>
|
||||||
|
<path d="M0.5,0.5 L9,9" transform="matrix(-1 0 0 1 9.5 0)"/>
|
||||||
|
</g>
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
build/icon.ico
BIN
build/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 361 KiB |
|
|
@ -1,5 +1,5 @@
|
||||||
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
|
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
|
||||||
import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE} 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 rpc from '../rpc';
|
||||||
import {userExitTermGroup, setActiveGroup} from './term-groups';
|
import {userExitTermGroup, setActiveGroup} from './term-groups';
|
||||||
|
|
||||||
|
|
@ -48,3 +48,36 @@ export function unmaximize() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openHamburgerMenu(coordinates) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: UI_OPEN_HAMBURGER_MENU,
|
||||||
|
effect() {
|
||||||
|
rpc.emit('open hamburger menu', coordinates);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minimize() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: UI_WINDOW_MINIMIZE,
|
||||||
|
effect() {
|
||||||
|
rpc.emit('minimize');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: UI_WINDOW_CLOSE,
|
||||||
|
effect() {
|
||||||
|
rpc.emit('close');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,12 @@ export function moveTo(i) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showPreferences() {
|
export function showPreferences() {
|
||||||
const editorFallback = process.platform === 'win32' ? 'notepad' : 'nano';
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
const command = process.platform === 'win32' ? ' start notepad "%userprofile%\\.hyper.js"' : ' bash -c \'exec env ${EDITOR:=nano} ~/.hyper.js\'';
|
||||||
|
const message = process.platform === 'win32' ?
|
||||||
|
' echo Attempting to open ^%userprofile^%\\.hyper.js with notepad' :
|
||||||
|
' echo Attempting to open ~/.hyper.js with your \$EDITOR'; // eslint-disable-line no-useless-escape
|
||||||
|
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_SHOW_PREFERENCES,
|
type: UI_SHOW_PREFERENCES,
|
||||||
|
|
@ -212,12 +217,12 @@ export function showPreferences() {
|
||||||
rpc.once('session data', () => {
|
rpc.once('session data', () => {
|
||||||
dispatch(sendSessionData(
|
dispatch(sendSessionData(
|
||||||
uid,
|
uid,
|
||||||
// Leading space prevents command to be stored in shell history
|
[ // Leading space prevents command to be stored in shell history
|
||||||
[' echo Attempting to open ~/.hyper.js with your \$EDITOR', // eslint-disable-line no-useless-escape
|
' echo Attempting to open ~/.hyper.js with your \$EDITOR', // eslint-disable-line no-useless-escape
|
||||||
' echo If it fails, open it manually with your favorite editor!',
|
message,
|
||||||
' bash -c \'exec env ${EDITOR:=' + editorFallback + '} ~/.hyper.js\'',
|
command,
|
||||||
''
|
''
|
||||||
].join('\n')
|
].join('\n\r')
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ export default class Header extends Component {
|
||||||
this.onChangeIntent = this.onChangeIntent.bind(this);
|
this.onChangeIntent = this.onChangeIntent.bind(this);
|
||||||
this.handleHeaderClick = this.handleHeaderClick.bind(this);
|
this.handleHeaderClick = this.handleHeaderClick.bind(this);
|
||||||
this.handleHeaderMouseDown = this.handleHeaderMouseDown.bind(this);
|
this.handleHeaderMouseDown = this.handleHeaderMouseDown.bind(this);
|
||||||
|
this.handleHamburgerMenuClick = this.handleHamburgerMenuClick.bind(this);
|
||||||
|
this.handleMaximizeClick = this.handleMaximizeClick.bind(this);
|
||||||
|
this.handleMinimizeClick = this.handleMinimizeClick.bind(this);
|
||||||
|
this.handleCloseClick = this.handleCloseClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeIntent(active) {
|
onChangeIntent(active) {
|
||||||
|
|
@ -68,11 +72,51 @@ export default class Header extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHamburgerMenuClick(event) {
|
||||||
|
let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect();
|
||||||
|
x -= 15; // to compensate padding
|
||||||
|
y -= 12; // ^ same
|
||||||
|
this.props.openHamburgerMenu({x, y});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMaximizeClick() {
|
||||||
|
if (this.props.maximized) {
|
||||||
|
this.props.unmaximize();
|
||||||
|
} else {
|
||||||
|
this.props.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMinimizeClick() {
|
||||||
|
this.props.minimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseClick() {
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
delete this.clicks;
|
delete this.clicks;
|
||||||
clearTimeout(this.clickTimer);
|
clearTimeout(this.clickTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWindowHeaderConfig() {
|
||||||
|
const {showHamburgerMenu, showWindowControls} = this.props;
|
||||||
|
const ret = {
|
||||||
|
hambMenu: process.platform === 'win32', // show by default on windows
|
||||||
|
winCtrls: !this.props.isMac // show by default on windows and linux
|
||||||
|
};
|
||||||
|
if (!this.props.isMac) { // allow the user to override the defaults if not on macOS
|
||||||
|
if (showHamburgerMenu !== '') {
|
||||||
|
ret.hambMenu = showHamburgerMenu;
|
||||||
|
}
|
||||||
|
if (showWindowControls !== '') {
|
||||||
|
ret.winCtrls = showWindowControls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
const {isMac} = this.props;
|
const {isMac} = this.props;
|
||||||
const props = getTabsProps(this.props, {
|
const props = getTabsProps(this.props, {
|
||||||
|
|
@ -81,11 +125,60 @@ export default class Header extends Component {
|
||||||
onClose: this.props.onCloseTab,
|
onClose: this.props.onCloseTab,
|
||||||
onChange: this.onChangeIntent
|
onChange: this.onChangeIntent
|
||||||
});
|
});
|
||||||
|
const {borderColor} = props;
|
||||||
|
let title = 'Hyper';
|
||||||
|
if (props.tabs.length === 1 && props.tabs[0].title) {
|
||||||
|
// if there's only one tab we use its title as the window title
|
||||||
|
title = props.tabs[0].title;
|
||||||
|
}
|
||||||
|
const {hambMenu, winCtrls} = this.getWindowHeaderConfig();
|
||||||
|
const left = winCtrls === 'left';
|
||||||
return (<header
|
return (<header
|
||||||
className={css('header', isMac && 'headerRounded')}
|
className={css('header', isMac && 'headerRounded')}
|
||||||
onClick={this.handleHeaderClick}
|
onClick={this.handleHeaderClick}
|
||||||
onMouseDown={this.handleHeaderMouseDown}
|
onMouseDown={this.handleHeaderMouseDown}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
!isMac &&
|
||||||
|
<div
|
||||||
|
className={css('windowHeader', props.tabs.length > 1 && 'windowHeaderWithBorder')}
|
||||||
|
style={{borderColor}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
hambMenu &&
|
||||||
|
<svg
|
||||||
|
className={css('shape', (left && 'hamburgerMenuRight') || 'hamburgerMenuLeft')}
|
||||||
|
onClick={this.handleHamburgerMenuClick}
|
||||||
|
>
|
||||||
|
<use xlinkHref="./dist/assets/icons.svg#hamburger-menu"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
<span className={css('appTitle')}>{title}</span>
|
||||||
|
{
|
||||||
|
winCtrls &&
|
||||||
|
<div className={css('windowControls', left && 'windowControlsLeft')}>
|
||||||
|
<svg
|
||||||
|
className={css('shape', left && 'minimizeWindowLeft')}
|
||||||
|
onClick={this.handleMinimizeClick}
|
||||||
|
>
|
||||||
|
<use xlinkHref="./dist/assets/icons.svg#minimize-window"/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
className={css('shape', left && 'maximizeWindowLeft')}
|
||||||
|
onClick={this.handleMaximizeClick}
|
||||||
|
>
|
||||||
|
<use xlinkHref="./dist/assets/icons.svg#maximize-window"/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
className={css('shape', 'closeWindow', left && 'closeWindowLeft')}
|
||||||
|
onClick={this.handleCloseClick}
|
||||||
|
>
|
||||||
|
<use xlinkHref="./dist/assets/icons.svg#close-window"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
<Tabs {...props}/>
|
<Tabs {...props}/>
|
||||||
{ this.props.customChildren }
|
{ this.props.customChildren }
|
||||||
|
|
@ -105,7 +198,77 @@ export default class Header extends Component {
|
||||||
headerRounded: {
|
headerRounded: {
|
||||||
borderTopLeftRadius: '4px',
|
borderTopLeftRadius: '4px',
|
||||||
borderTopRightRadius: '4px'
|
borderTopRightRadius: '4px'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
windowHeader: {
|
||||||
|
height: '34px',
|
||||||
|
width: '100%',
|
||||||
|
position: 'fixed',
|
||||||
|
top: '1px',
|
||||||
|
left: '1px',
|
||||||
|
right: '1px',
|
||||||
|
WebkitAppRegion: 'drag',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
windowHeaderWithBorder: {
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
borderBottomWidth: '1px'
|
||||||
|
},
|
||||||
|
|
||||||
|
appTitle: {
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: `-apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", "Roboto", "Oxygen",
|
||||||
|
"Ubuntu", "Cantarell", "Fira Sans",
|
||||||
|
"Droid Sans", "Helvetica Neue", sans-serif`
|
||||||
|
},
|
||||||
|
|
||||||
|
shape: {
|
||||||
|
width: '40px',
|
||||||
|
height: '34px',
|
||||||
|
padding: '12px 15px 12px 15px',
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
color: '#808080',
|
||||||
|
shapeRendering: 'crispEdges',
|
||||||
|
':hover': {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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'}}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default class Tab extends Component {
|
||||||
onClick={this.props.onClose}
|
onClick={this.props.onClose}
|
||||||
>
|
>
|
||||||
<svg className={css('shape')}>
|
<svg className={css('shape')}>
|
||||||
<use xlinkHref="./dist/assets/icons.svg#close"/>
|
<use xlinkHref="./dist/assets/icons.svg#close-tab"/>
|
||||||
</svg>
|
</svg>
|
||||||
</i>
|
</i>
|
||||||
{ this.props.customChildren }
|
{ this.props.customChildren }
|
||||||
|
|
@ -185,7 +185,8 @@ export default class Tab extends Component {
|
||||||
width: '6px',
|
width: '6px',
|
||||||
height: '6px',
|
height: '6px',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
fill: 'currentColor'
|
fill: 'currentColor',
|
||||||
|
shapeRendering: 'crispEdges'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,40 +18,45 @@ export default class Tabs extends Component {
|
||||||
onClose
|
onClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (<nav className={css('nav')}>
|
const hide = !isMac && tabs.length === 1;
|
||||||
|
|
||||||
|
return (<nav className={css('nav', hide && 'hiddenNav')}>
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
{
|
{
|
||||||
tabs.length ?
|
tabs.length === 1 && isMac ?
|
||||||
tabs.length === 1 ?
|
<div className={css('title')}>{tabs[0].title}</div> :
|
||||||
<div className={css('title')}>{tabs[0].title}</div> :
|
null
|
||||||
[
|
}
|
||||||
<ul
|
{
|
||||||
key="list"
|
tabs.length > 1 ?
|
||||||
className={css('list')}
|
[
|
||||||
>
|
<ul
|
||||||
{
|
key="list"
|
||||||
tabs.map((tab, i) => {
|
className={css('list')}
|
||||||
const {uid, title, isActive, hasActivity} = tab;
|
>
|
||||||
const props = getTabProps(tab, this.props, {
|
{
|
||||||
text: title === '' ? 'Shell' : title,
|
tabs.map((tab, i) => {
|
||||||
isFirst: i === 0,
|
const {uid, title, isActive, hasActivity} = tab;
|
||||||
isLast: tabs.length - 1 === i,
|
const props = getTabProps(tab, this.props, {
|
||||||
borderColor,
|
text: title === '' ? 'Shell' : title,
|
||||||
isActive,
|
isFirst: i === 0,
|
||||||
hasActivity,
|
isLast: tabs.length - 1 === i,
|
||||||
onSelect: onChange.bind(null, uid),
|
borderColor,
|
||||||
onClose: onClose.bind(null, uid)
|
isActive,
|
||||||
});
|
hasActivity,
|
||||||
return <Tab key={`tab-${uid}`} {...props}/>;
|
onSelect: onChange.bind(null, uid),
|
||||||
})
|
onClose: onClose.bind(null, uid)
|
||||||
}
|
});
|
||||||
</ul>,
|
return <Tab key={`tab-${uid}`} {...props}/>;
|
||||||
isMac && <div
|
})
|
||||||
key="shim"
|
}
|
||||||
style={{borderColor}}
|
</ul>,
|
||||||
className={css('borderShim')}
|
isMac && <div
|
||||||
/>
|
key="shim"
|
||||||
] :
|
style={{borderColor}}
|
||||||
|
className={css('borderShim')}
|
||||||
|
/>
|
||||||
|
] :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{ this.props.customChildren }
|
{ this.props.customChildren }
|
||||||
|
|
@ -73,7 +78,12 @@ export default class Tabs extends Component {
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
WebkitAppRegion: 'drag'
|
WebkitAppRegion: isMac ? 'drag' : '',
|
||||||
|
top: isMac ? '' : '34px'
|
||||||
|
},
|
||||||
|
|
||||||
|
hiddenNav: {
|
||||||
|
display: 'none'
|
||||||
},
|
},
|
||||||
|
|
||||||
title: {
|
title: {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {decorate, getTermGroupProps} from '../utils/plugins';
|
||||||
import TermGroup_ from './term-group';
|
import TermGroup_ from './term-group';
|
||||||
|
|
||||||
const TermGroup = decorate(TermGroup_, 'TermGroup');
|
const TermGroup = decorate(TermGroup_, 'TermGroup');
|
||||||
|
const isMac = /Mac/.test(navigator.userAgent);
|
||||||
|
|
||||||
export default class Terms extends Component {
|
export default class Terms extends Component {
|
||||||
|
|
||||||
|
|
@ -71,8 +72,9 @@ export default class Terms extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
|
const shift = !isMac && this.props.termGroups.length > 1;
|
||||||
return (<div
|
return (<div
|
||||||
className={css('terms')}
|
className={css('terms', shift && 'termsShifted')}
|
||||||
>
|
>
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +134,12 @@ export default class Terms extends Component {
|
||||||
right: 0,
|
right: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
color: '#fff'
|
color: '#fff',
|
||||||
|
transition: isMac ? '' : 'margin-top 0.3s ease'
|
||||||
|
},
|
||||||
|
|
||||||
|
termsShifted: {
|
||||||
|
marginTop: '68px'
|
||||||
},
|
},
|
||||||
|
|
||||||
termGroup: {
|
termGroup: {
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,6 @@ export const UI_WINDOW_MOVE = 'UI_WINDOW_MOVE';
|
||||||
export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
|
export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
|
||||||
export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
|
export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
|
||||||
export const UI_OPEN_FILE = 'UI_OPEN_FILE';
|
export const UI_OPEN_FILE = 'UI_OPEN_FILE';
|
||||||
|
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';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import {createSelector} from 'reselect';
|
import {createSelector} from 'reselect';
|
||||||
|
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
import {closeTab, changeTab, maximize, unmaximize} from '../actions/header';
|
import {closeTab, changeTab, maximize, openHamburgerMenu, unmaximize, minimize, close} from '../actions/header';
|
||||||
import {connect} from '../utils/plugins';
|
import {connect} from '../utils/plugins';
|
||||||
import getRootGroups from '../selectors';
|
import getRootGroups from '../selectors';
|
||||||
|
|
||||||
|
|
@ -35,7 +35,9 @@ const HeaderContainer = connect(
|
||||||
activeMarkers: state.ui.activityMarkers,
|
activeMarkers: state.ui.activityMarkers,
|
||||||
borderColor: state.ui.borderColor,
|
borderColor: state.ui.borderColor,
|
||||||
backgroundColor: state.ui.backgroundColor,
|
backgroundColor: state.ui.backgroundColor,
|
||||||
maximized: state.ui.maximized
|
maximized: state.ui.maximized,
|
||||||
|
showHamburgerMenu: state.ui.showHamburgerMenu,
|
||||||
|
showWindowControls: state.ui.showWindowControls
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
dispatch => {
|
dispatch => {
|
||||||
|
|
@ -54,6 +56,18 @@ const HeaderContainer = connect(
|
||||||
|
|
||||||
unmaximize: () => {
|
unmaximize: () => {
|
||||||
dispatch(unmaximize());
|
dispatch(unmaximize());
|
||||||
|
},
|
||||||
|
|
||||||
|
openHamburgerMenu: coordinates => {
|
||||||
|
dispatch(openHamburgerMenu(coordinates));
|
||||||
|
},
|
||||||
|
|
||||||
|
minimize: () => {
|
||||||
|
dispatch(minimize());
|
||||||
|
},
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
dispatch(close());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ const TermsContainer = connect(
|
||||||
},
|
},
|
||||||
|
|
||||||
onTitle(uid, title) {
|
onTitle(uid, title) {
|
||||||
dispatch(setSessionXtermTitle(uid, title));
|
// we need to trim the title because `cmd.exe` likes to report ' ' as the title
|
||||||
|
dispatch(setSessionXtermTitle(uid, title.trim()));
|
||||||
},
|
},
|
||||||
|
|
||||||
onResize(uid, cols, rows) {
|
onResize(uid, cols, rows) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import {values} from '../utils/object';
|
||||||
|
|
||||||
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
|
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
|
||||||
const allowedBells = new Set(['SOUND', false]);
|
const allowedBells = new Set(['SOUND', false]);
|
||||||
|
const allowedHamburgerMenuValues = new Set([true, false]);
|
||||||
|
const allowedWindowControlsValues = new Set([true, false, 'left']);
|
||||||
|
|
||||||
// Populate `config-default.js` from this :)
|
// Populate `config-default.js` from this :)
|
||||||
const initial = Immutable({
|
const initial = Immutable({
|
||||||
|
|
@ -79,7 +81,9 @@ const initial = Immutable({
|
||||||
modifierKeys: {
|
modifierKeys: {
|
||||||
altIsMeta: false,
|
altIsMeta: false,
|
||||||
cmdIsMeta: false
|
cmdIsMeta: false
|
||||||
}
|
},
|
||||||
|
showHamburgerMenu: '',
|
||||||
|
showWindowControls: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const reducer = (state = initial, action) => {
|
const reducer = (state = initial, action) => {
|
||||||
|
|
@ -171,6 +175,14 @@ const reducer = (state = initial, action) => {
|
||||||
ret.modifierKeys = config.modifierKeys;
|
ret.modifierKeys = config.modifierKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowedHamburgerMenuValues.has(config.showHamburgerMenu)) {
|
||||||
|
ret.showHamburgerMenu = config.showHamburgerMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedWindowControlsValues.has(config.showWindowControls)) {
|
||||||
|
ret.showWindowControls = config.showWindowControls;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
})());
|
})());
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"start": "concurrently --kill-others --raw \"npm run dev\" \"npm run app\"",
|
"start": "concurrently --kill-others --raw \"npm run dev\" \"npm run app\"",
|
||||||
"app": "electron app",
|
"app": "electron app",
|
||||||
"dev": "webpack -w",
|
"dev": "webpack -w",
|
||||||
"build": "NODE_ENV=production webpack",
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
"test": "npm run dist && xo && ava",
|
"test": "npm run dist && xo && ava",
|
||||||
"prepush": "npm test",
|
"prepush": "npm test",
|
||||||
"postinstall": "install-app-deps",
|
"postinstall": "install-app-deps",
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
"babel-preset-react": "^6.11.1",
|
"babel-preset-react": "^6.11.1",
|
||||||
"concurrently": "^3.0.0",
|
"concurrently": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^4.0.0",
|
"copy-webpack-plugin": "^4.0.0",
|
||||||
|
"cross-env": "3.1.3",
|
||||||
"electron": "1.4.5",
|
"electron": "1.4.5",
|
||||||
"electron-builder": "^7.11.4",
|
"electron-builder": "^7.11.4",
|
||||||
"electron-devtools-installer": "^2.0.0",
|
"electron-devtools-installer": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ test.before(async () => {
|
||||||
pathToBinary = path.join(__dirname, '../dist/mac/Hyper.app/Contents/MacOS/Hyper');
|
pathToBinary = path.join(__dirname, '../dist/mac/Hyper.app/Contents/MacOS/Hyper');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'win32':
|
||||||
|
pathToBinary = path.join(__dirname, '../dist/win-unpacked/Hyper.exe');
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error('Path to the built binary needs to be defined for this platform in test/index.js');
|
throw new Error('Path to the built binary needs to be defined for this platform in test/index.js');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue