├── .eslintrc ├── .gitignore ├── LICENCE.md ├── README.md ├── images ├── tray-hover.png ├── tray-hover@2x.png ├── tray2Template.png ├── tray2Template@2x.png ├── trayTemplate.png └── trayTemplate@2x.png ├── index.js ├── overlay.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | "tab" 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "quotes": [ 20 | "error", 21 | "single" 22 | ], 23 | "semi": [ 24 | "error", 25 | "always" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .DS_Store 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2016 Henrique Moreira and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperTerm Overlay 2 | 3 | A complete and customizable solution for a permanent / dropdown / hotkey / overlay window in your Hyper.app, accessible via hotkeys and/or toolbar icon (tray). 4 | 5 | **Important:** Designed for Hyper.app >= 0.7.0 6 | 7 | ![home2](https://cloud.githubusercontent.com/assets/924158/17121698/d122bcaa-52ab-11e6-876c-25a267d00e89.gif) 8 | 9 | ## Install 10 | 11 | Edit your `~/.hyper.js` (`Cmd+,`) and insert the `hyperterm-overlay` in your `plugins` array: 12 | ```js 13 | plugins: [ 14 | 'hyperterm-overlay' 15 | ], 16 | ``` 17 | 18 | ## Configuration 19 | 20 | Add `overlay` in your `~/.hyper.js` config. 21 | The configuration below shows all possibilities with their respective default values. 22 | 23 | ```js 24 | module.exports = { 25 | config: { 26 | // other configs... 27 | overlay: { 28 | alwaysOnTop: true, 29 | animate: true, 30 | hasShadow: false, 31 | hideDock: false, 32 | hideOnBlur: false, 33 | hotkeys: ['Option+Space'], 34 | position: 'top', 35 | primaryDisplay: false, 36 | resizable: true, 37 | startAlone: false, 38 | startup: false, 39 | size: 0.4, 40 | tray: true, 41 | unique: false 42 | } 43 | }, 44 | //... 45 | }; 46 | ``` 47 | 48 | ### alwaysOnTop 49 | - Value: true or false 50 | - Default: true 51 | - Makes Hyperterm Overlay window stay always on top. 52 | 53 | ### animate 54 | - Value: true or false 55 | - Default: true 56 | - Enable animation when show and hide the window. 57 | 58 | ### hasShadow 59 | - Value: true or false 60 | - Default: false 61 | - Controls the default macOS window shadows. 62 | 63 | ### hideOnBlur 64 | - Value: true or false 65 | - Default: false 66 | - Hides the HyperTerm Overlay when it loses focus. 67 | 68 | ### hideDock 69 | - Value: true or false 70 | - Default: false 71 | - Removes the HyperTerm dock icon. It works only when the `unique` option is activated. 72 | 73 | ### hotkeys 74 | - Value: array of hotkey strings 75 | - Default: ['Option+Space'] 76 | - Specify one or more hotkeys to show and hide the HyperTerm Overlay (see: [`Accelerator`](https://github.com/electron/electron/blob/master/docs/api/accelerator.md)) 77 | 78 | ### position 79 | - Value: 'top', 'bottom', 'left' or 'right' 80 | - Default: 'top' 81 | - Choose where HyperTerm Overlay will be positioned: `top`, `bottom`, `left` or `right`. 82 | 83 | ### primaryDisplay 84 | - Value: true or false 85 | - Default: false 86 | - Show HyperTerm Overlay only on primary display. 87 | 88 | ### resizable 89 | - Value: true or false 90 | - Default: true 91 | - Allow the HyperTerm Overlay be resizable. 92 | 93 | ![resize](https://cloud.githubusercontent.com/assets/924158/17121469/5281a916-52aa-11e6-92f5-fa1c3dff75c8.gif) 94 | 95 | ### size 96 | - Value: float or number 97 | - Default: 0.4 98 | - The size of HyperTerm Overlay when it is showing. 99 | The possible values are a `number` or a `float`. 100 | If is a number, it represents the size um pixels. 101 | Else, if is a float, it means the percentage of the screen. 102 | 103 | ### startAlone 104 | - Value: true or false 105 | - Default: false 106 | - Makes HyperTerm Overlay the unique window displayed when started. 107 | - Other windows started will be default Hyper.app windows. 108 | 109 | ### startup 110 | - Value: true or false 111 | - Default: true 112 | - Open HyperTerm Overlay on Hyper.app startup. 113 | 114 | ### tray 115 | - Value: true or false 116 | - Default: true 117 | - Add icon to the system notification area, for access HyperTerm Overlay. 118 | 119 | ![tray](https://cloud.githubusercontent.com/assets/924158/17121470/5294b02e-52aa-11e6-9bca-9d70f186c60b.gif) 120 | 121 | ### unique 122 | - Value: true or false 123 | - Default: false 124 | - Makes HyperTerm Overlay the unique window of Hyper.app. Any other window will be removed. 125 | 126 | ## Licence 127 | 128 | [MIT](LICENSE.md) 129 | -------------------------------------------------------------------------------- /images/tray-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/tray-hover.png -------------------------------------------------------------------------------- /images/tray-hover@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/tray-hover@2x.png -------------------------------------------------------------------------------- /images/tray2Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/tray2Template.png -------------------------------------------------------------------------------- /images/tray2Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/tray2Template@2x.png -------------------------------------------------------------------------------- /images/trayTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/trayTemplate.png -------------------------------------------------------------------------------- /images/trayTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickgbw/hyperterm-overlay/ce8c570eb6af150ecd6526d2d6eaf1dd9b936b67/images/trayTemplate@2x.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const overlay = require('./overlay'); 4 | 5 | exports.onApp = app => { 6 | overlay.registerApp(app); 7 | }; 8 | 9 | exports.onWindow = win => { 10 | overlay.registerWindow(win); 11 | }; 12 | 13 | exports.onUnload = () => { 14 | overlay.destroy(); 15 | }; 16 | 17 | exports.decorateBrowserOptions = config => { 18 | return overlay.decorateBrowserOptions(config); 19 | }; 20 | 21 | exports.decorateMenu = menu => { 22 | return menu.map( 23 | item => { 24 | if (item.label !== 'Plugins') return item; 25 | const newItem = Object.assign({}, item); 26 | newItem.submenu = newItem.submenu.concat({ 27 | label: 'Show/Hide Overlay', 28 | click: () => overlay.interact() 29 | }); 30 | return newItem; 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /overlay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | const electron = require('electron'); 5 | const {BrowserWindow,globalShortcut,Tray,Menu,nativeImage} = electron; 6 | const path = require('path'); 7 | 8 | //check if platform is Mac 9 | const isMac = process.platform === 'darwin'; 10 | 11 | class Overlay { 12 | constructor() { 13 | this._app = null; 14 | this._win = null; 15 | this._creatingWindow = false; 16 | this._decoratingWindow = false; 17 | this._animating = false; 18 | this._config = {}; 19 | this._tray = null; 20 | this._trayAnimation = null; 21 | this._lastFocus = null; 22 | this._forceStartup = false; 23 | 24 | //store tray images 25 | this._trayImage = nativeImage.createFromPath(path.join(__dirname, 'images', 'trayTemplate.png')); 26 | this._tray2Image = nativeImage.createFromPath(path.join(__dirname, 'images', 'tray2Template.png')); 27 | this._trayPressedImage = nativeImage.createFromPath(path.join(__dirname, 'images', 'tray-hover.png')); 28 | } 29 | 30 | //app started 31 | registerApp(app) { 32 | let startup = false; 33 | 34 | //subscribe for config changes only on first time 35 | if(!this._app) { 36 | app.config.subscribe(() => { 37 | if(this._win) 38 | this._refreshConfig(true); 39 | }); 40 | startup = true; 41 | } 42 | 43 | //load user configs 44 | this._app = app; 45 | 46 | //creating the overlay window 47 | this._create(() => { 48 | //open on startup 49 | if((startup && this._config.startup) || this._forceStartup) 50 | this.show(); 51 | }); 52 | } 53 | 54 | //checks if new window could be created 55 | registerWindow(win) { 56 | if(!this._creatingWindow && this._config.unique) 57 | win.close(); 58 | else if(this._decoratingWindow) 59 | this._decoratingWindow = false; 60 | } 61 | 62 | //creating a new overlay window 63 | _create(fn) { 64 | if(this._win) return; 65 | 66 | this._creatingWindow = true; 67 | this._decoratingWindow = true; 68 | 69 | this._app.createWindow(win => { 70 | this._win = win; 71 | 72 | //apply configurations 73 | this._refreshConfig(); 74 | 75 | this._setConfigs(win); 76 | 77 | //hide window when loses focus 78 | win.on('blur', () => { 79 | if(this._config.hideOnBlur) 80 | this.hide(); 81 | }); 82 | 83 | //store the new size selected 84 | win.on('resize', () => { 85 | if(this._config.resizable && !this._creatingWindow && !this._animating) { 86 | switch(this._config.position) { 87 | case 'top': 88 | case 'bottom': 89 | this._config.size = win.getSize()[1]; 90 | break; 91 | 92 | case 'right': 93 | case 'left': 94 | this._config.size = win.getSize()[0]; 95 | break; 96 | } 97 | } 98 | }); 99 | 100 | //permanent window 101 | win.on('closed', () => { 102 | this._win = null; 103 | this._clearTrayAnimation(); 104 | }); 105 | 106 | //forces hide initially 107 | win.hide(); 108 | 109 | //activate terminal 110 | win.rpc.emit('termgroup add req'); 111 | 112 | //callback 113 | this._creatingWindow = false; 114 | if(fn) fn(); 115 | }); 116 | } 117 | 118 | //apply user overlay window configs 119 | _refreshConfig(reapply) { 120 | //get user configs 121 | let userConfig = this._app.config.getConfig().overlay; 122 | 123 | //comparing old and new configs 124 | if(userConfig) { 125 | //hide dock icon or not, check before apply 126 | if(userConfig.unique && userConfig.hideDock) 127 | this._app.dock.hide(); 128 | else if(this._config.unique && this._config.hideDock) 129 | this._app.dock.show(); 130 | 131 | //removing the initial windows of hyperterm 132 | if((userConfig.unique && !this._config.unique) || (userConfig.startAlone && !reapply)) { 133 | this._app.getWindows().forEach(win => { 134 | if(win != this._win) 135 | win.close(); 136 | }); 137 | } 138 | } 139 | 140 | //default configuration 141 | this._config = { 142 | alwaysOnTop: true, 143 | animate: true, 144 | hasShadow: false, 145 | hideDock: false, 146 | hideOnBlur: false, 147 | hotkeys: ['Option+Space'], 148 | position: 'top', 149 | primaryDisplay: false, 150 | resizable: true, 151 | size: 0.4, 152 | startAlone: false, 153 | startup: false, 154 | tray: true, 155 | unique: false 156 | }; 157 | 158 | //replacing user preferences 159 | if(userConfig) Object.assign(this._config,userConfig); 160 | 161 | //registering the hotkeys 162 | globalShortcut.unregisterAll(); 163 | for(let hotkey of this._config.hotkeys) 164 | globalShortcut.register(hotkey, () => this.interact()); 165 | 166 | //tray icon 167 | let trayCreated = false; 168 | if(this._config.tray && !this._tray) { 169 | //prevent destroy / create bug 170 | this._tray = new Tray(this._trayImage); 171 | this._tray.setToolTip('Open HyperTerm Overlay'); 172 | this._tray.setPressedImage(this._trayPressedImage); 173 | this._tray.on('click', () => this.interact()); 174 | trayCreated = true; 175 | } else if(!this._config.tray && this._tray) { 176 | this._clearTrayAnimation(); 177 | this._tray.destroy(); 178 | this._tray = null; 179 | } 180 | 181 | 182 | if(reapply && this._win) { 183 | this._setConfigs(this._win); 184 | this._endBounds(this._win); 185 | //animate tray 186 | if(this._win.isVisible() && trayCreated) 187 | this._animateTray(); 188 | } 189 | } 190 | 191 | //change windows settings for the overlay window 192 | _setConfigs(win) { 193 | win.setHasShadow(this._config.hasShadow); 194 | win.setResizable(this._config.resizable); 195 | win.setAlwaysOnTop(this._config.alwaysOnTop); 196 | } 197 | 198 | //get current display 199 | _getDisplay() { 200 | const screen = electron.screen; 201 | let display = (!this._config.primaryDisplay) ? screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) : screen.getPrimaryDisplay(); 202 | return display; 203 | } 204 | 205 | _startBounds() { 206 | const {x, y, width, height} = this._getDisplay().workArea; 207 | 208 | switch(this._config.position) { 209 | case 'left': 210 | this._win.setBounds({ x: x, y: y, width: 1, height: height }, this._config.animate); 211 | break; 212 | case 'right': 213 | this._win.setBounds({ x: x + width - 1, y: y, width: 1, height: height }, this._config.animate); 214 | break; 215 | case 'bottom': 216 | this._win.setBounds({ x: x, y: y + height, width: width, height: 0 }, this._config.animate); 217 | break; 218 | default: 219 | case 'top': 220 | this._win.setBounds({ x: x, y: y, width: width, height: 0 }, this._config.animate); 221 | break; 222 | } 223 | } 224 | 225 | //set window bounds according to config 226 | _endBounds() { 227 | const {x, y, width, height} = this._getDisplay().workArea; 228 | let size; 229 | 230 | //end position 231 | switch(this._config.position) { 232 | case 'left': 233 | size = (this._config.size > 1) ? this._config.size : Math.round(width * this._config.size); 234 | this._win.setBounds({ x: x, y: y, width: size, height: height }, this._config.animate); 235 | break; 236 | case 'bottom': 237 | size = (this._config.size > 1) ? this._config.size : Math.round(height * this._config.size); 238 | this._win.setBounds({ x: x, y: y + height - size, width: width, height: size }, this._config.animate); 239 | break; 240 | case 'right': 241 | size = (this._config.size > 1) ? this._config.size : Math.round(width * this._config.size); 242 | this._win.setBounds({ x: width - size, y: y, width: size, height: height }, this._config.animate); 243 | break; 244 | default: 245 | case 'top': 246 | size = (this._config.size > 1) ? this._config.size : Math.round(height * this._config.size); 247 | this._win.setBounds({ x: x, y: y, width: width, height: size }, this._config.animate); 248 | break; 249 | } 250 | } 251 | 252 | //tray animation when overlay window is open 253 | _animateTray() { 254 | if(!this._config.tray || !this._tray) return; 255 | 256 | //tool tip 257 | this._tray.setToolTip('Close HyperTerm Overlay'); 258 | 259 | if(isMac) { 260 | if(this._trayAnimation) clearInterval(this._trayAnimation); 261 | let type = 0; 262 | this._trayAnimation = setInterval(() => { 263 | if(this._tray) 264 | this._tray.setImage((++type % 2) ? this._trayImage : this._tray2Image); 265 | },400); 266 | } 267 | } 268 | 269 | //finish tray animation 270 | _clearTrayAnimation() { 271 | if(this._trayAnimation) clearInterval(this._trayAnimation); 272 | 273 | if(this._tray) { 274 | this._tray.setToolTip('Open HyperTerm Overlay'); 275 | if(isMac) this._tray.setImage(this._trayImage); 276 | } 277 | } 278 | 279 | //setting initial configuration for the new window 280 | decorateBrowserOptions(config) { 281 | if(this._decoratingWindow) { 282 | return Object.assign({}, config, { 283 | titleBarStyle: '', 284 | frame: false, 285 | minWidth: 0, 286 | minHeight: 0, 287 | maximizable: false, 288 | minimizable: false, 289 | movable: false, 290 | show: false 291 | }); 292 | } else 293 | return config; 294 | } 295 | 296 | //open or close overlay window 297 | interact() { 298 | if(!this._win) { 299 | //re-create overlay window and show 300 | this._create(() => this.show()); 301 | return; 302 | } 303 | 304 | if(!this._win.isVisible()) 305 | this.show(); 306 | else 307 | this.hide(); 308 | } 309 | 310 | //show the overlay window 311 | show() { 312 | if(!this._win || this._animating || this._win.isVisible()) return; 313 | 314 | //store internal window focus 315 | this._lastFocus = BrowserWindow.getFocusedWindow(); 316 | 317 | //set window initial bounds (for animation) 318 | if(this._config.animate) { 319 | this._animating = true; 320 | setTimeout(() => { 321 | this._animating = false; 322 | },250); 323 | this._startBounds(); 324 | } 325 | 326 | //show and focus window 327 | this._win.show(); 328 | this._win.focus(); 329 | 330 | //set end bounds 331 | this._endBounds(); 332 | 333 | this._animateTray(); 334 | } 335 | 336 | //hides the overlay window 337 | hide() { 338 | if(!this._win || this._animating || !this._win.isVisible()) return; 339 | 340 | //search for the better previous windows focus 341 | let findFocus = () => { 342 | if(this._win.isFocused()) { 343 | //chose internal or external focus 344 | if(this._lastFocus && this._lastFocus.sessions && this._lastFocus.sessions.size) 345 | this._lastFocus.focus(); 346 | else if(isMac) 347 | Menu.sendActionToFirstResponder('hide:'); 348 | } 349 | }; 350 | 351 | //control the animation 352 | if(this._config.animate) { 353 | this._animating = true; 354 | 355 | //animation end bounds 356 | this._startBounds(); 357 | 358 | setTimeout(() => { 359 | this._animating = false; 360 | findFocus(); 361 | this._win.hide(); 362 | }, 250); 363 | } 364 | //close without animation 365 | else { 366 | findFocus(); 367 | this._win.hide(); 368 | } 369 | 370 | this._clearTrayAnimation(); 371 | } 372 | 373 | //unload everything applied 374 | destroy() { 375 | if(this._tray) { 376 | this._tray.destroy(); 377 | this._tray = null; 378 | } 379 | if(this._win) { 380 | //open again if is a plugin reload 381 | this._forceStartup = (this._win.isVisible()); 382 | this._win.close(); 383 | this._win = null; 384 | } 385 | globalShortcut.unregisterAll(); 386 | this._creatingWindow = false; 387 | this._decoratingWindow = false; 388 | this._animating = false; 389 | this._config = {}; 390 | this._lastFocus = null; 391 | } 392 | } 393 | 394 | module.exports = new Overlay(); 395 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperterm-overlay", 3 | "version": "0.4.0", 4 | "description": "A complete and customizable solution for a permanent, dropdown, hotkey and overlay window in your Hyper.app", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint", 8 | "lint": "eslint ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rickgbw/hyperterm-overlay.git" 13 | }, 14 | "author": "rickgbw", 15 | "keywords": [ 16 | "hyperterm", 17 | "hyper.app", 18 | "hyper", 19 | "terminal", 20 | "overlay", 21 | "hotkey", 22 | "window", 23 | "visor", 24 | "iterm", 25 | "guake", 26 | "yakuake", 27 | "dropdown", 28 | "iterm2" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/rickgbw/hyperterm-overlay/issues" 33 | }, 34 | "homepage": "https://github.com/rickgbw/hyperterm-overlay", 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "eslint": "^3.1.1" 38 | } 39 | } 40 | --------------------------------------------------------------------------------