├── .eslintrc.js ├── .github └── issue_template.md ├── .gitignore ├── .prettierignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── __mocks__ └── hyperterm-register-shortcut.js ├── __tests__ └── index.test.js ├── fixtures ├── app.js └── windowSet.js ├── index.js ├── jest.config.js ├── lint-staged.config.js ├── modules ├── __tests__ │ ├── app.test.js │ ├── dispose.test.js │ ├── toggle.test.js │ └── windows.test.js ├── app.js ├── dispose.js ├── toggle.js └── windows.js ├── package.json ├── prettier.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['standard', 'prettier', 'prettier/standard'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'prettier/prettier': ['error', { singleQuote: true, trailingComma: 'es5' }], 6 | }, 7 | globals: { 8 | afterAll: true, 9 | afterEach: true, 10 | beforeAll: true, 11 | beforeEach: true, 12 | describe: true, 13 | expect: true, 14 | fdescribe: true, 15 | fit: true, 16 | it: true, 17 | jest: true, 18 | xdescribe: true, 19 | xit: true, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | - [ ] I am on the [latest](https://github.com/soutar/hyperterm-summon/releases/latest) hyperterm-summon version 12 | - [ ] I have searched the [issues](https://github.com/soutar/hyperterm-summon/issues) of this repo and believe that this is not a duplicate 13 | 14 | 18 | 19 | - **OS version and name**: 20 | - **Hyper.app version**: 21 | - **hyperterm-summon version**: 22 | - **Link of a [Gist](https://gist.github.com/) with the contents of your .hyper.js**: 23 | - **Relevant information from devtools** _(CMD+ALT+I on Mac OS, CTRL+SHIFT+I elsewhere)_: 24 | 25 | --- 26 | 27 | ## Issue 28 | 29 | 30 | 31 | ### Steps to Reproduce 32 | 33 | 1. 34 | 2. 35 | 36 | ### Expected Result 37 | 38 | 39 | 40 | ### Actual Result 41 | 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .tool-versions 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | notifications: 5 | email: false 6 | script: 7 | - yarn lint 8 | - yarn test 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Bug Reporting and Improvements 4 | 5 | Please open issues for any bugs encountered or ideas for improvements for the project. 6 | 7 | ## Modifying Code 8 | 9 | Bug fixes and improvements are welcomed. Please verify your code changes are formatted with the project's Prettier configuration and the test suite passes. Adding new tests for your code changes is also encouraged. 10 | 11 | ## Publishing a Release 12 | 13 | 1. Run `yarn release` to publish a new release. (See `release` [CLI documentation](https://github.com/vercel/release) for more information.) 14 | 1. For each change, use the `release` CLI to select the appropriate semver type or ignore. 15 | 1. In the browser window that opens, set the GitHub Release title to the version number of the release, e.g. `2.0.11`. 16 | 1. Publish the GitHub Release. 17 | 1. In your shell, run `npm publish` to publish the new release to the npm registry. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperterm-summon 2 | 3 | Summon your Hyper windows with a system-wide hotkey. 4 | 5 | In a multi-window situation, hyperterm-summon will remember the last active 6 | window and restore focus to it. 7 | 8 | If Hyper is already active when the hotkey is pressed, your terminal windows 9 | will be hidden and (on macOS only) your previously-active application will 10 | regain focus. 11 | 12 | ## Installation 13 | 14 | Use the Hyper CLI, bundled with your Hyper app, to install hyperterm-summon 15 | by entering the following into Hyper: 16 | 17 | ```bash 18 | hyper i hyperterm-summon 19 | ``` 20 | 21 | ## Options 22 | 23 | | Key | Description | Default | 24 | | ------------ | ------------------------------------------------------- | -------- | 25 | | `hideDock` | Hide the Hyper icon in the dock and app switcher. | `false` | 26 | | `hideOnBlur` | Hide Hyper when the windows lose focus. | `false` | 27 | | `hotkey` | Shortcut1 to toggle Hyper window visibility. | `Ctrl+;` | 28 | 29 | ## Example Config 30 | 31 | ```js 32 | module.exports = { 33 | config: { 34 | summon: { 35 | hideDock: true, 36 | hideOnBlur: true, 37 | hotkey: 'Alt+Super+O', 38 | }, 39 | }, 40 | plugins: ['hyperterm-summon'], 41 | }; 42 | ``` 43 | 44 | 1 For a list of valid shortcuts, see [Electron Accelerators](https://github.com/electron/electron/blob/master/docs/api/accelerator.md). 45 | -------------------------------------------------------------------------------- /__mocks__/hyperterm-register-shortcut.js: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn(() => jest.fn()); 2 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | applyConfig, 3 | generateActivateCallback, 4 | onApp: summonOnApp, 5 | } = require('../modules/app'); 6 | const { generateApp } = require('../fixtures/app'); 7 | const { 8 | generateBlurCallback, 9 | hideWindows, 10 | showWindows, 11 | } = require('../modules/windows'); 12 | const { onApp, onUnload } = require('../index'); 13 | const dispose = require('../modules/dispose'); 14 | 15 | jest.mock('../modules/app'); 16 | jest.mock('../modules/windows'); 17 | jest.mock('../modules/dispose'); 18 | 19 | const app = generateApp(); 20 | 21 | describe('onApp', () => { 22 | beforeEach(() => { 23 | onApp(app); 24 | }); 25 | 26 | it('generates a blur handler', () => { 27 | expect(generateBlurCallback).toHaveBeenCalledWith(hideWindows, app); 28 | }); 29 | 30 | it('generates a activate handler', () => { 31 | expect(generateActivateCallback).toHaveBeenCalledWith(showWindows, app); 32 | }); 33 | 34 | it('executes hyperterm-summon onApp', () => { 35 | expect(summonOnApp).toHaveBeenCalledWith( 36 | app, 37 | expect.any(Function), 38 | undefined 39 | ); 40 | }); 41 | }); 42 | 43 | describe('onUnload', () => { 44 | beforeEach(() => { 45 | onUnload(app); 46 | }); 47 | 48 | it('executes dispose', () => { 49 | expect(dispose).toHaveBeenCalledTimes(1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /fixtures/app.js: -------------------------------------------------------------------------------- 1 | exports.generateApp = opts => 2 | Object.assign( 3 | { 4 | config: { 5 | getConfig: jest.fn(() => ({})), 6 | subscribe: jest.fn(), 7 | }, 8 | createWindow: jest.fn(), 9 | dock: { 10 | hide: jest.fn(), 11 | show: jest.fn(), 12 | }, 13 | getLastFocusedWindow: jest.fn(), 14 | getWindows: jest.fn(() => new Set()), 15 | hide: jest.fn(), 16 | listeners: jest.fn(() => []), 17 | on: jest.fn(), 18 | removeListener: jest.fn(), 19 | show: jest.fn(), 20 | }, 21 | opts 22 | ); 23 | -------------------------------------------------------------------------------- /fixtures/windowSet.js: -------------------------------------------------------------------------------- 1 | const generateWindowSet = (count, opts = {}) => { 2 | const windows = []; 3 | 4 | for (var i = 0; i < count; i++) { 5 | windows.push(generateWindow(opts)); 6 | } 7 | 8 | return new Set(windows); 9 | }; 10 | 11 | const generateWindow = ({ 12 | focused = false, 13 | fullScreen = false, 14 | visible = true, 15 | } = {}) => ({ 16 | focus: jest.fn(), 17 | hide: jest.fn(), 18 | isFocused: jest.fn(() => focused), 19 | isFullScreen: jest.fn(() => fullScreen), 20 | isVisible: jest.fn(() => visible), 21 | minimize: jest.fn(), 22 | on: jest.fn(), 23 | show: jest.fn(), 24 | }); 25 | 26 | module.exports = { 27 | generateWindow, 28 | generateWindowSet, 29 | }; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const dispose = require('./modules/dispose'); 2 | const { 3 | applyConfig, 4 | generateActivateCallback, 5 | onApp, 6 | } = require('./modules/app'); 7 | const { 8 | generateBlurCallback, 9 | hideWindows, 10 | showWindows, 11 | } = require('./modules/windows'); 12 | 13 | let cfgUnsubscribe, handleActivate, handleBlur; 14 | 15 | exports.onApp = app => { 16 | handleBlur = generateBlurCallback(hideWindows, app); 17 | handleActivate = generateActivateCallback(showWindows, app); 18 | cfgUnsubscribe = onApp( 19 | app, 20 | applyConfig.bind(this, app, handleBlur), 21 | handleActivate 22 | ); 23 | }; 24 | 25 | exports.onUnload = app => { 26 | dispose(app, { cfgUnsubscribe, handleActivate, handleBlur }); 27 | }; 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coveragePathIgnorePatterns: ['/node_modules', '/(fixtures)/'], 4 | testPathIgnorePatterns: ['/node_modules/', '/utils/test.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.js': ['prettier --write', 'git add'], 3 | }; 4 | -------------------------------------------------------------------------------- /modules/__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | const registerShortcut = require('hyperterm-register-shortcut'); 2 | const toggle = require('../toggle'); 3 | const { applyConfig, generateActivateCallback, onApp } = require('../app'); 4 | const { generateApp } = require('../../fixtures/app'); 5 | 6 | jest.mock('../toggle'); 7 | jest.mock('../windows'); 8 | jest.mock('hyperterm-register-shortcut'); 9 | 10 | let app = generateApp(); 11 | let callback; 12 | const handleBlurMock = jest.fn(); 13 | const generateActivateCallbackMock = jest.fn(); 14 | 15 | describe('applyConfig', () => { 16 | describe('with default config', () => { 17 | beforeEach(() => { 18 | applyConfig(app, handleBlurMock); 19 | }); 20 | 21 | it('registers the default hot key', () => { 22 | expect(registerShortcut).toHaveBeenCalledWith('summon', toggle, 'Ctrl+;'); 23 | }); 24 | 25 | it('shows the dock', () => { 26 | expect(app.dock.show).toHaveBeenCalled(); 27 | }); 28 | 29 | it('does not handle blur events', () => { 30 | expect(app.removeListener).toHaveBeenCalledWith( 31 | 'browser-window-blur', 32 | handleBlurMock 33 | ); 34 | }); 35 | }); 36 | 37 | describe('with hideDock config enabled', () => { 38 | beforeEach(() => { 39 | app.config.getConfig.mockReturnValue({ summon: { hideDock: true } }); 40 | applyConfig(app, handleBlurMock); 41 | }); 42 | 43 | it('hides the dock', () => { 44 | expect(app.dock.hide).toHaveBeenCalled(); 45 | }); 46 | }); 47 | 48 | describe('with hideOnBlur config enabled', () => { 49 | beforeEach(() => { 50 | app.config.getConfig.mockReturnValue({ 51 | summon: { 52 | hideOnBlur: true, 53 | }, 54 | }); 55 | }); 56 | 57 | afterEach(() => { 58 | Array.prototype.includes.mockRestore(); 59 | }); 60 | 61 | describe('with no previous handler', () => { 62 | beforeEach(() => { 63 | jest.spyOn(Array.prototype, 'includes').mockReturnValue(false); 64 | applyConfig(app, handleBlurMock); 65 | }); 66 | 67 | it('adds blur handler', () => { 68 | expect(app.on).toHaveBeenCalledWith( 69 | 'browser-window-blur', 70 | handleBlurMock 71 | ); 72 | }); 73 | }); 74 | 75 | describe('with previous handler', () => { 76 | beforeEach(() => { 77 | jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); 78 | applyConfig(app, handleBlurMock); 79 | }); 80 | 81 | it('does not add handler', () => { 82 | expect(app.on).not.toHaveBeenCalledWith( 83 | 'browser-window-blur', 84 | handleBlurMock 85 | ); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('when dock is undefined', () => { 91 | it('does not throw error', () => { 92 | const appMock = generateApp({ dock: undefined }); 93 | expect(() => applyConfig(appMock, handleBlurMock)).not.toThrow(); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('generateActivateCallback', () => { 99 | beforeAll(() => { 100 | callback = jest.fn(); 101 | }); 102 | 103 | it('resulting callback shows the windows', () => { 104 | generateActivateCallback(callback, app)(); 105 | 106 | expect(callback).toHaveBeenCalledTimes(1); 107 | }); 108 | }); 109 | 110 | describe('onApp', () => { 111 | describe('with default config', () => { 112 | beforeEach(() => { 113 | callback = jest.fn(); 114 | onApp(app, callback, handleBlurMock, generateActivateCallbackMock); 115 | }); 116 | 117 | it('handles the activate event', () => { 118 | expect(app.on).toHaveBeenCalledWith('activate', expect.any(Function)); 119 | }); 120 | 121 | it('executes the callback', () => { 122 | expect(callback).toHaveBeenCalledTimes(1); 123 | }); 124 | 125 | it('subscribes to config change', () => { 126 | expect(app.config.subscribe).toHaveBeenCalledWith(callback); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /modules/__tests__/dispose.test.js: -------------------------------------------------------------------------------- 1 | const dispose = require('../dispose'); 2 | const { generateApp } = require('../../fixtures/app'); 3 | // const { unregisterShortcut } = require('hyperterm-register-shortcut') 4 | 5 | // jest.mock('hyperterm-register-shortcut') 6 | jest.mock('../windows'); 7 | 8 | const app = generateApp(); 9 | const handleActivateMock = jest.fn(); 10 | const handleBlurMock = jest.fn(); 11 | const cfgUnsubscribeMock = jest.fn(); 12 | 13 | describe('dispose', () => { 14 | beforeEach(() => { 15 | dispose(app, { 16 | cfgUnsubscribe: cfgUnsubscribeMock, 17 | handleActivate: handleActivateMock, 18 | handleBlur: handleBlurMock, 19 | }); 20 | }); 21 | 22 | it('unsubscribes from config changes', () => { 23 | expect(cfgUnsubscribeMock).toHaveBeenCalledTimes(1); 24 | }); 25 | 26 | xit('unregisters the shortcut', () => { 27 | // expect(unregisterShortcut).toHaveBeenCalledTimes(1) 28 | }); 29 | 30 | it('removes the activate listener', () => { 31 | expect(app.removeListener).toHaveBeenCalledWith( 32 | 'activate', 33 | handleActivateMock 34 | ); 35 | }); 36 | 37 | it('removes the blur listener', () => { 38 | expect(app.removeListener).toHaveBeenCalledWith( 39 | 'browser-window-blur', 40 | handleBlurMock 41 | ); 42 | }); 43 | 44 | describe('without config unsubscribe', () => { 45 | it('does not throw an error', () => { 46 | expect(() => 47 | dispose(app, { 48 | handleActivate: handleActivateMock, 49 | handleBlur: handleBlurMock, 50 | }) 51 | ).not.toThrow(); 52 | }); 53 | }); 54 | 55 | describe('without activate callback', () => { 56 | it('does not remove activate listener', () => { 57 | dispose(app, { 58 | cfgUnsubscribe: cfgUnsubscribeMock, 59 | handleBlur: handleBlurMock, 60 | }); 61 | expect(app.removeListener).not.toHaveBeenCalledWith( 62 | 'activate', 63 | undefined 64 | ); 65 | }); 66 | }); 67 | 68 | describe('without blur callback', () => { 69 | it('does not remove blur listener', () => { 70 | dispose(app, { 71 | cfgUnsubscribe: cfgUnsubscribeMock, 72 | handleActivate: handleActivateMock, 73 | }); 74 | expect(app.removeListener).not.toHaveBeenCalledWith( 75 | 'browser-window-blur', 76 | undefined 77 | ); 78 | }); 79 | }); 80 | 81 | describe('on unsupported platforms', () => { 82 | it('does not show the dock', () => { 83 | app.dock = undefined; 84 | expect(() => dispose(app)).not.toThrow(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /modules/__tests__/toggle.test.js: -------------------------------------------------------------------------------- 1 | const toggle = require('../toggle'); 2 | const { generateApp } = require('../../fixtures/app'); 3 | const { 4 | generateWindow, 5 | generateWindowSet, 6 | } = require('../../fixtures/windowSet'); 7 | const { hideWindows, showWindows } = require('../windows'); 8 | 9 | jest.mock('../windows'); 10 | 11 | const app = generateApp(); 12 | let win; 13 | let set = generateWindowSet(2); 14 | 15 | describe('toggle', () => { 16 | beforeAll(() => { 17 | app.getWindows.mockReturnValue(set); 18 | }); 19 | 20 | describe('when windows blurred', () => { 21 | beforeEach(() => { 22 | toggle(app); 23 | }); 24 | 25 | it('shows the windows', () => { 26 | expect(showWindows).toHaveBeenCalledTimes(1); 27 | }); 28 | }); 29 | 30 | describe('when windows focused', () => { 31 | beforeAll(() => { 32 | win = generateWindow(); 33 | win.isFocused.mockReturnValue(true); 34 | set.add(win); 35 | }); 36 | 37 | beforeEach(() => { 38 | toggle(app); 39 | }); 40 | 41 | afterAll(() => { 42 | set.delete(win); 43 | }); 44 | 45 | it('hides windows', () => { 46 | expect(hideWindows).toHaveBeenCalledTimes(1); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /modules/__tests__/windows.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | generateBlurCallback, 3 | hideWindows, 4 | showWindows, 5 | } = require('../windows'); 6 | const { 7 | generateWindow, 8 | generateWindowSet, 9 | } = require('../../fixtures/windowSet'); 10 | const { generateApp } = require('../../fixtures/app'); 11 | 12 | jest.useFakeTimers(); 13 | 14 | const app = generateApp(); 15 | let callback, win, hiddenApp, hiddenSet; 16 | let set = generateWindowSet(2); 17 | 18 | describe('generateBlurCallback', () => { 19 | beforeAll(() => { 20 | callback = jest.fn(); 21 | app.getWindows.mockReturnValue(set); 22 | }); 23 | 24 | describe('when no focused windows', () => { 25 | it('executes the callback', () => { 26 | generateBlurCallback(callback, app)(); 27 | 28 | jest.runAllTimers(); 29 | 30 | expect(callback).toHaveBeenCalledTimes(1); 31 | }); 32 | }); 33 | 34 | describe('when focused windows', () => { 35 | beforeAll(() => { 36 | win = generateWindow(); 37 | win.isFocused.mockReturnValue(true); 38 | set.add(win); 39 | }); 40 | 41 | afterAll(() => { 42 | set.delete(win); 43 | }); 44 | 45 | it('does not execute the callback', () => { 46 | generateBlurCallback(callback, app)(); 47 | 48 | jest.runAllTimers(); 49 | 50 | expect(callback).not.toHaveBeenCalled(); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('hideWindows', () => { 56 | beforeAll(() => { 57 | app.getWindows.mockReturnValue(set); 58 | }); 59 | 60 | it('gets the last focused window', () => { 61 | hideWindows(app); 62 | 63 | expect(app.getLastFocusedWindow).toHaveBeenCalled(); 64 | }); 65 | 66 | describe('when no visible windows', () => { 67 | beforeAll(() => { 68 | hiddenApp = generateApp(); 69 | hiddenSet = generateWindowSet(3, { visible: false }); 70 | hiddenApp.getWindows.mockReturnValue(hiddenSet); 71 | }); 72 | 73 | it('does not hide windows', () => { 74 | process.platform = 'darwin'; 75 | hideWindows(hiddenApp); 76 | expect([...hiddenSet][0].hide).not.toHaveBeenCalled(); 77 | expect([...hiddenSet][1].hide).not.toHaveBeenCalled(); 78 | }); 79 | 80 | it('does not minimize windows', () => { 81 | process.platform = 'win32'; 82 | hideWindows(hiddenApp); 83 | expect([...hiddenSet][0].minimize).not.toHaveBeenCalled(); 84 | expect([...hiddenSet][1].minimize).not.toHaveBeenCalled(); 85 | }); 86 | 87 | it('does not get last focused window', () => { 88 | hideWindows(hiddenApp); 89 | 90 | expect(hiddenApp.getLastFocusedWindow).not.toHaveBeenCalled(); 91 | }); 92 | }); 93 | 94 | describe('when full screen window', () => { 95 | beforeAll(() => { 96 | win = generateWindow(); 97 | win.isFullScreen.mockReturnValue(true); 98 | set.add(win); 99 | }); 100 | 101 | beforeEach(() => { 102 | hideWindows(app); 103 | }); 104 | 105 | afterAll(() => { 106 | set.delete(win); 107 | }); 108 | 109 | it('does not minimize', () => { 110 | process.platform = 'win32'; 111 | expect(win.minimize).not.toHaveBeenCalled(); 112 | }); 113 | 114 | it('does not hide', () => { 115 | process.platform = 'darwin'; 116 | expect(win.hide).not.toHaveBeenCalled(); 117 | }); 118 | }); 119 | 120 | describe('when hiding window supported', () => { 121 | it('hides windows', () => { 122 | hideWindows(app); 123 | expect([...set][0].hide).toHaveBeenCalledTimes(1); 124 | expect([...set][1].hide).toHaveBeenCalledTimes(1); 125 | }); 126 | }); 127 | 128 | describe('when hiding window unsupported', () => { 129 | it('minimizes windows', () => { 130 | let originalImplementation; 131 | set.forEach(w => { 132 | originalImplementation = w.hide; 133 | w.hide = null; 134 | }); 135 | hideWindows(app); 136 | 137 | expect([...set][0].minimize).toHaveBeenCalledTimes(1); 138 | 139 | set.forEach(w => { 140 | w.hide = originalImplementation; 141 | }); 142 | }); 143 | }); 144 | 145 | describe('when hiding app supported', () => { 146 | it('hides the app', () => { 147 | hideWindows(app); 148 | expect(app.hide).toHaveBeenCalledTimes(1); 149 | }); 150 | }); 151 | 152 | describe('when hiding app unsupported', () => { 153 | it('does not throw an error', () => { 154 | const originalImplementation = app.hide; 155 | app.hide = null; 156 | expect(() => hideWindows(app)).not.toThrowError(); 157 | app.hide = originalImplementation; 158 | }); 159 | }); 160 | }); 161 | 162 | describe('showWindows', () => { 163 | describe('when zero windows', () => { 164 | beforeAll(() => { 165 | app.getWindows.mockReturnValue(new Set()); 166 | }); 167 | 168 | it('creates one window', () => { 169 | showWindows(app); 170 | 171 | expect(app.createWindow).toHaveBeenCalledTimes(1); 172 | }); 173 | }); 174 | 175 | describe('when existing windows', () => { 176 | beforeAll(() => { 177 | app.getWindows.mockReturnValue(set); 178 | }); 179 | 180 | beforeEach(() => { 181 | showWindows(app); 182 | }); 183 | 184 | it('creates zero wins', () => { 185 | expect(app.createWindow).not.toHaveBeenCalled(); 186 | }); 187 | 188 | it('shows each window', () => { 189 | expect([...set][0].show).toHaveBeenCalledTimes(1); 190 | expect([...set][1].show).toHaveBeenCalledTimes(1); 191 | }); 192 | 193 | xit('focuses the last active window', () => { 194 | expect([...set][0].focus).not.toHaveBeenCalled(); 195 | expect([...set][1].focus).toHaveBeenCalledTimes(1); 196 | }); 197 | }); 198 | 199 | describe('on supported platforms', () => { 200 | it('shows the app', () => { 201 | app.show = jest.fn(); 202 | showWindows(app); 203 | 204 | expect(app.show).toHaveBeenCalledTimes(1); 205 | }); 206 | }); 207 | 208 | describe('on unsupported platforms', () => { 209 | it('does not throw an error', () => { 210 | app.show = null; 211 | expect(() => showWindows(app)).not.toThrowError(); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /modules/app.js: -------------------------------------------------------------------------------- 1 | const registerShortcut = require('hyperterm-register-shortcut'); 2 | const toggle = require('./toggle'); 3 | 4 | const DEFAULTS = { 5 | hideDock: false, 6 | hideOnBlur: false, 7 | hotkey: 'Ctrl+;', 8 | }; 9 | 10 | const applyConfig = (app, handleBlur) => { 11 | const config = Object.assign({}, DEFAULTS, app.config.getConfig().summon); 12 | 13 | // TODO: Unregister prior to registering when supported 14 | registerShortcut('summon', toggle, DEFAULTS.hotkey)(app); 15 | 16 | if (app.dock) { 17 | config.hideDock ? app.dock.hide() : app.dock.show(); 18 | } 19 | 20 | if (!config.hideOnBlur) { 21 | app.removeListener('browser-window-blur', handleBlur); 22 | } else if (!app.listeners('browser-window-blur').includes(handleBlur)) { 23 | app.on('browser-window-blur', handleBlur); 24 | } 25 | }; 26 | 27 | const generateActivateCallback = (callback, app) => event => callback(app); 28 | 29 | const onApp = (app, callback, handleActivate) => { 30 | app.on('activate', handleActivate); 31 | callback(); 32 | return app.config.subscribe(callback); 33 | }; 34 | 35 | module.exports = { 36 | applyConfig, 37 | generateActivateCallback, 38 | onApp, 39 | }; 40 | -------------------------------------------------------------------------------- /modules/dispose.js: -------------------------------------------------------------------------------- 1 | // const { unregisterShortcut } = require('hyperterm-register-shortcut') 2 | 3 | module.exports = (app, callbacks = {}) => { 4 | const { cfgUnsubscribe, handleActivate, handleBlur } = callbacks; 5 | 6 | // TODO: Unregister shortcut when supported 7 | // unregisterShortcut() 8 | 9 | cfgUnsubscribe && cfgUnsubscribe(); 10 | handleActivate && app.removeListener('activate', handleActivate); 11 | handleBlur && app.removeListener('browser-window-blur', handleBlur); 12 | }; 13 | -------------------------------------------------------------------------------- /modules/toggle.js: -------------------------------------------------------------------------------- 1 | const { hideWindows, showWindows } = require('./windows'); 2 | 3 | module.exports = app => { 4 | // @NOTE: Linux reports blurred windows as focused, so also use isVisible 5 | const focusedWindows = [...app.getWindows()].filter( 6 | w => w.isFocused() && w.isVisible() 7 | ); 8 | 9 | focusedWindows.length > 0 ? hideWindows(app) : showWindows(app); 10 | }; 11 | -------------------------------------------------------------------------------- /modules/windows.js: -------------------------------------------------------------------------------- 1 | const { debounce } = require('lodash'); 2 | 3 | const isFocused = function(w) { 4 | return w === this.getLastFocusedWindow(); 5 | }; 6 | 7 | exports.generateBlurCallback = (callback, app) => 8 | debounce(() => { 9 | const focusedWindows = [...app.getWindows()].some(w => w.isFocused()); 10 | 11 | if (focusedWindows) { 12 | return false; 13 | } 14 | 15 | callback(app); 16 | }, 100); 17 | 18 | exports.hideWindows = app => { 19 | const visibleWindows = [...app.getWindows()].filter(w => w.isVisible()); 20 | 21 | if (!visibleWindows.length) { 22 | return false; 23 | } 24 | 25 | visibleWindows.sort(isFocused.bind(app)).forEach(w => { 26 | if (w.isFullScreen()) { 27 | return; 28 | } 29 | 30 | if (typeof w.hide === 'function') { 31 | w.hide(); 32 | } else { 33 | w.minimize(); 34 | } 35 | }); 36 | 37 | if (typeof app.hide === 'function') { 38 | app.hide(); 39 | } 40 | }; 41 | 42 | exports.showWindows = app => { 43 | const windows = [...app.getWindows()].sort(isFocused.bind(app)); 44 | 45 | windows.length === 0 ? app.createWindow() : windows.forEach(w => w.show()); 46 | 47 | if (typeof app.show === 'function') { 48 | app.show(); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperterm-summon", 3 | "version": "2.0.11", 4 | "description": "Summon your Hyperterm windows with a system-wide hotkey", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Soutar/hyperterm-summon.git" 9 | }, 10 | "scripts": { 11 | "format": "prettier '**/*.{css,md,js,json}' --write", 12 | "lint": "eslint '{index,{__mocks__,fixtures,modules}/**/*}.js'", 13 | "precommit": "lint-staged", 14 | "test": "jest" 15 | }, 16 | "keywords": [ 17 | "hyper", 18 | "hyper.app", 19 | "hyper-plugin", 20 | "plugin", 21 | "hotkey", 22 | "shortcut", 23 | "global", 24 | "system", 25 | "focus", 26 | "summon" 27 | ], 28 | "author": "John Soutar ", 29 | "license": "ISC", 30 | "dependencies": { 31 | "hyperterm-register-shortcut": "^1.3.0", 32 | "lodash": "^4.17.11" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^6.0.1", 36 | "eslint-config-prettier": "^6.0.0", 37 | "eslint-config-standard": "^12.0.0", 38 | "eslint-plugin-import": "^2.18.0", 39 | "eslint-plugin-node": "^9.1.0", 40 | "eslint-plugin-prettier": "^3.1.0", 41 | "eslint-plugin-promise": "^4.2.1", 42 | "eslint-plugin-standard": "^4.0.0", 43 | "husky": "^2.7.0", 44 | "jest": "^24.8.0", 45 | "lint-staged": "^8.2.1", 46 | "prettier": "^1.18.2", 47 | "release": "^6.3.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | }; 5 | --------------------------------------------------------------------------------