diff --git a/README.md b/README.md index 86af0afb..951b6b64 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ $ brew cask install hyper ## 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 3. Install the dependencies: `npm install` 4. Build the code, watch for changes and run the app: `npm start` diff --git a/app/config-default.js b/app/config-default.js index 66db6c4d..8d1072a1 100644 --- a/app/config-default.js +++ b/app/config-default.js @@ -27,6 +27,16 @@ module.exports = { // custom css to embed in the terminal window 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`) padding: '12px 14px', diff --git a/app/index.js b/app/index.js index 7bf09f49..ae761ea6 100644 --- a/app/index.js +++ b/app/index.js @@ -1,3 +1,5 @@ +// eslint-disable-next-line curly, unicorn/no-process-exit +if (require('electron-squirrel-startup')) process.exit(); // Native const {resolve} = require('path'); @@ -108,9 +110,11 @@ app.on('ready', () => installDevExtensions(isDev).then(() => { height, minHeight: 190, minWidth: 370, - titleBarStyle: 'hidden-inset', + titleBarStyle: 'hidden-inset', // macOS only title: 'Hyper.app', backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'), + // we want to go frameless on windows and linux + frame: process.platform === 'darwin', transparent: true, icon: resolve(__dirname, 'static/icon.png'), // 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.on('open hamburger menu', ({x, y}) => { + Menu.getApplicationMenu().popup(x, y); + }); + + rpc.on('minimize', () => { + win.minimize(); + }); + + rpc.on('close', () => { + win.close(); + }); + const deleteSessions = () => { sessions.forEach((session, key) => { session.removeAllListeners(); @@ -305,7 +321,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => { }); win.on('closed', () => { - if (process.platform !== 'darwin') { + if (process.platform !== 'darwin' && windowSet.size === 0) { app.quit(); } }); diff --git a/app/package.json b/app/package.json index 670e6318..bddee34e 100644 --- a/app/package.json +++ b/app/package.json @@ -11,18 +11,19 @@ "repository": "zeit/hyper", "xo": false, "dependencies": { - "child_pty": "3.0.1", "color": "0.11.3", "convert-css-color-name-to-hex": "0.1.1", "default-shell": "1.0.1", "electron-config": "0.2.1", "electron-is-dev": "0.1.1", + "electron-squirrel-startup": "1.0.0", "file-uri-to-path": "0.0.2", "gaze": "1.1.2", "git-describe": "3.0.2", "mkdirp": "0.5.1", "ms": "0.7.1", "node-fetch": "1.6.3", + "pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642", "semver": "5.3.0", "shell-env": "0.2.0", "uuid": "2.0.2" diff --git a/app/plugins.js b/app/plugins.js index f5b26b6d..a2d83f82 100644 --- a/app/plugins.js +++ b/app/plugins.js @@ -236,7 +236,7 @@ function install(fn) { env.npm_config_target = '1.3.0'; env.npm_config_disturl = 'https://atom.io/download/atom-shell'; /* eslint-enable camelcase */ - exec('npm prune; npm install --production', { + exec('npm prune && npm install --production', { cwd: path, env, shell diff --git a/app/session.js b/app/session.js index 9f75439f..3b9f08f7 100644 --- a/app/session.js +++ b/app/session.js @@ -8,16 +8,13 @@ const {getDecoratedEnv} = require('./plugins'); const {productName, version} = require('./package'); 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; try { - spawn = require('child_pty').spawn; + spawn = require('pty.js').spawn; } catch (err) { - console.error( - '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' - ); + throw createPtyJsError(); } const envFromConfig = config.getConfig().env || {}; @@ -37,12 +34,20 @@ module.exports = class Session extends EventEmitter { const defaultShellArgs = ['--login']; - this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, { - columns, - rows, - cwd, - env: getDecoratedEnv(baseEnv) - }); + try { + this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, { + columns, + rows, + 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 => { if (this.ended) { @@ -69,9 +74,9 @@ module.exports = class Session extends EventEmitter { this.pty.stdin.write(data); } - resize({cols: columns, rows}) { + resize({cols, rows}) { try { - this.pty.stdout.resize({columns, rows}); + this.pty.stdout.resize(cols, rows); } catch (err) { console.error(err.stack); } @@ -79,7 +84,7 @@ module.exports = class Session extends EventEmitter { destroy() { try { - this.pty.kill('SIGHUP'); + this.pty.kill(); } catch (err) { console.error('exit error', err.stack); } diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..6cd75c18 --- /dev/null +++ b/appveyor.yml @@ -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 diff --git a/assets/icons.svg b/assets/icons.svg index 482a666b..71a7b8e0 100644 --- a/assets/icons.svg +++ b/assets/icons.svg @@ -1,8 +1,48 @@ - - close + + close tab + + hamburger menu + + + + + + minimize window + + + + + + + + + + + maximize window + + + + + + + + + + + + + close window + + + + + + + + diff --git a/build/icon.ico b/build/icon.ico index c857abd9..3984592d 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/lib/actions/header.js b/lib/actions/header.js index bfc05faf..3b5ed07b 100644 --- a/lib/actions/header.js +++ b/lib/actions/header.js @@ -1,5 +1,5 @@ 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 {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'); + } + }); + }; +} diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 7e9ab35f..133aa774 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -201,7 +201,12 @@ export function moveTo(i) { } 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 => { dispatch({ type: UI_SHOW_PREFERENCES, @@ -212,12 +217,12 @@ export function showPreferences() { rpc.once('session data', () => { dispatch(sendSessionData( uid, - // 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 If it fails, open it manually with your favorite editor!', - ' bash -c \'exec env ${EDITOR:=' + editorFallback + '} ~/.hyper.js\'', - '' - ].join('\n') + [ // 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 + message, + command, + '' + ].join('\n\r') )); }); }); diff --git a/lib/components/header.js b/lib/components/header.js index 8daea89c..a02b9afe 100644 --- a/lib/components/header.js +++ b/lib/components/header.js @@ -14,6 +14,10 @@ export default class Header extends Component { this.onChangeIntent = this.onChangeIntent.bind(this); this.handleHeaderClick = this.handleHeaderClick.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) { @@ -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() { delete this.clicks; 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) { const {isMac} = this.props; const props = getTabsProps(this.props, { @@ -81,11 +125,60 @@ export default class Header extends Component { onClose: this.props.onCloseTab, 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 (
+ { + !isMac && +
1 && 'windowHeaderWithBorder')} + style={{borderColor}} + > + { + hambMenu && + + + + } + {title} + { + winCtrls && +
+ + + + + + + + + +
+ } +
+ } { this.props.customChildrenBefore } { this.props.customChildren } @@ -105,7 +198,77 @@ export default class Header extends Component { headerRounded: { borderTopLeftRadius: '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'}} }; } diff --git a/lib/components/tab.js b/lib/components/tab.js index a51eb8d1..7d3ef5d2 100644 --- a/lib/components/tab.js +++ b/lib/components/tab.js @@ -79,7 +79,7 @@ export default class Tab extends Component { onClick={this.props.onClose} > - + { this.props.customChildren } @@ -185,7 +185,8 @@ export default class Tab extends Component { width: '6px', height: '6px', verticalAlign: 'middle', - fill: 'currentColor' + fill: 'currentColor', + shapeRendering: 'crispEdges' } }; } diff --git a/lib/components/tabs.js b/lib/components/tabs.js index a9811328..26150b24 100644 --- a/lib/components/tabs.js +++ b/lib/components/tabs.js @@ -18,40 +18,45 @@ export default class Tabs extends Component { onClose } = this.props; - return (