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:
Matheus Fernandes 2016-11-11 15:18:04 -02:00 committed by GitHub
parent 26701d43b5
commit 9c90e19760
21 changed files with 433 additions and 74 deletions

View file

@ -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`

View file

@ -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',

View file

@ -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();
}
});

View file

@ -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"

View file

@ -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

View file

@ -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);
}

31
appveyor.yml Normal file
View 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

View file

@ -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">
<defs>
<symbol id="close" viewBox="0 0 24 24">
<title>close</title>
<symbol id="close-tab" viewBox="0 0 24 24">
<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>
</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>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View file

@ -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');
}
});
};
}

View file

@ -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')
));
});
});

View file

@ -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 (<header
className={css('header', isMac && 'headerRounded')}
onClick={this.handleHeaderClick}
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 }
<Tabs {...props}/>
{ 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'}}
};
}

View file

@ -79,7 +79,7 @@ export default class Tab extends Component {
onClick={this.props.onClose}
>
<svg className={css('shape')}>
<use xlinkHref="./dist/assets/icons.svg#close"/>
<use xlinkHref="./dist/assets/icons.svg#close-tab"/>
</svg>
</i>
{ 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'
}
};
}

View file

@ -18,40 +18,45 @@ export default class Tabs extends Component {
onClose
} = this.props;
return (<nav className={css('nav')}>
const hide = !isMac && tabs.length === 1;
return (<nav className={css('nav', hide && 'hiddenNav')}>
{ this.props.customChildrenBefore }
{
tabs.length ?
tabs.length === 1 ?
<div className={css('title')}>{tabs[0].title}</div> :
[
<ul
key="list"
className={css('list')}
>
{
tabs.map((tab, i) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props}/>;
})
}
</ul>,
isMac && <div
key="shim"
style={{borderColor}}
className={css('borderShim')}
/>
] :
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) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props}/>;
})
}
</ul>,
isMac && <div
key="shim"
style={{borderColor}}
className={css('borderShim')}
/>
] :
null
}
{ this.props.customChildren }
@ -73,7 +78,12 @@ export default class Tabs extends Component {
cursor: 'default',
position: 'relative',
WebkitUserSelect: 'none',
WebkitAppRegion: 'drag'
WebkitAppRegion: isMac ? 'drag' : '',
top: isMac ? '' : '34px'
},
hiddenNav: {
display: 'none'
},
title: {

View file

@ -4,6 +4,7 @@ import {decorate, getTermGroupProps} from '../utils/plugins';
import TermGroup_ from './term-group';
const TermGroup = decorate(TermGroup_, 'TermGroup');
const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends Component {
@ -71,8 +72,9 @@ export default class Terms extends Component {
}
template(css) {
const shift = !isMac && this.props.termGroups.length > 1;
return (<div
className={css('terms')}
className={css('terms', shift && 'termsShifted')}
>
{ this.props.customChildrenBefore }
{
@ -132,7 +134,12 @@ export default class Terms extends Component {
right: 0,
left: 0,
bottom: 0,
color: '#fff'
color: '#fff',
transition: isMac ? '' : 'margin-top 0.3s ease'
},
termsShifted: {
marginTop: '68px'
},
termGroup: {

View file

@ -13,3 +13,6 @@ export const UI_WINDOW_MOVE = 'UI_WINDOW_MOVE';
export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
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';

View file

@ -2,7 +2,7 @@
import {createSelector} from 'reselect';
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 getRootGroups from '../selectors';
@ -35,7 +35,9 @@ const HeaderContainer = connect(
activeMarkers: state.ui.activityMarkers,
borderColor: state.ui.borderColor,
backgroundColor: state.ui.backgroundColor,
maximized: state.ui.maximized
maximized: state.ui.maximized,
showHamburgerMenu: state.ui.showHamburgerMenu,
showWindowControls: state.ui.showWindowControls
};
},
dispatch => {
@ -54,6 +56,18 @@ const HeaderContainer = connect(
unmaximize: () => {
dispatch(unmaximize());
},
openHamburgerMenu: coordinates => {
dispatch(openHamburgerMenu(coordinates));
},
minimize: () => {
dispatch(minimize());
},
close: () => {
dispatch(close());
}
};
}

View file

@ -46,7 +46,8 @@ const TermsContainer = connect(
},
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) {

View file

@ -22,6 +22,8 @@ import {values} from '../utils/object';
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
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 :)
const initial = Immutable({
@ -79,7 +81,9 @@ const initial = Immutable({
modifierKeys: {
altIsMeta: false,
cmdIsMeta: false
}
},
showHamburgerMenu: '',
showWindowControls: ''
});
const reducer = (state = initial, action) => {
@ -171,6 +175,14 @@ const reducer = (state = initial, action) => {
ret.modifierKeys = config.modifierKeys;
}
if (allowedHamburgerMenuValues.has(config.showHamburgerMenu)) {
ret.showHamburgerMenu = config.showHamburgerMenu;
}
if (allowedWindowControlsValues.has(config.showWindowControls)) {
ret.showWindowControls = config.showWindowControls;
}
return ret;
})());
break;

View file

@ -4,7 +4,7 @@
"start": "concurrently --kill-others --raw \"npm run dev\" \"npm run app\"",
"app": "electron app",
"dev": "webpack -w",
"build": "NODE_ENV=production webpack",
"build": "cross-env NODE_ENV=production webpack",
"test": "npm run dist && xo && ava",
"prepush": "npm test",
"postinstall": "install-app-deps",
@ -92,6 +92,7 @@
"babel-preset-react": "^6.11.1",
"concurrently": "^3.0.0",
"copy-webpack-plugin": "^4.0.0",
"cross-env": "3.1.3",
"electron": "1.4.5",
"electron-builder": "^7.11.4",
"electron-devtools-installer": "^2.0.0",

View file

@ -19,6 +19,10 @@ test.before(async () => {
pathToBinary = path.join(__dirname, '../dist/mac/Hyper.app/Contents/MacOS/Hyper');
break;
case 'win32':
pathToBinary = path.join(__dirname, '../dist/win-unpacked/Hyper.exe');
break;
default:
throw new Error('Path to the built binary needs to be defined for this platform in test/index.js');
}