diff --git a/app/main.js b/app/main.js index 47443468..9ad1acfb 100644 --- a/app/main.js +++ b/app/main.js @@ -1,12 +1,23 @@ +const electron = require('electron') + +let app = electron.app + +let secondInstance = app.makeSingleInstance((argv) => { + app.window.webContents.send('host:second-instance') +}) + +if (secondInstance) { + app.quit() + return +} + + const yaml = require('js-yaml') const path = require('path') const fs = require('fs') const Config = require('electron-config') -const electron = require('electron') const platform = require('os').platform() require('electron-debug')({enabled: true, showDevTools: process.argv.indexOf('--debug') != -1}) - -let app = electron.app let windowConfig = new Config({name: 'window'}) @@ -14,7 +25,8 @@ setupWindowManagement = () => { let windowCloseable app.window.on('show', () => { - electron.ipcMain.send('window-shown') + app.window.focus() + app.window.webContents.send('host:window-shown') }) app.window.on('close', (e) => { @@ -46,6 +58,14 @@ setupWindowManagement = () => { }) electron.ipcMain.on('window-maximize', () => { + app.window.maximize() + }) + + electron.ipcMain.on('window-unmaximize', () => { + app.window.unmaximize() + }) + + electron.ipcMain.on('window-toggle-maximize', () => { if (app.window.isMaximized()) { app.window.unmaximize() } else { @@ -61,6 +81,10 @@ setupWindowManagement = () => { app.window.setBounds(bounds, true) }) + electron.ipcMain.on('window-set-always-on-top', (event, flag) => { + app.window.setAlwaysOnTop(flag) + }) + app.on('before-quit', () => windowCloseable = true) } @@ -95,15 +119,6 @@ setupMenu = () => { start = () => { let t0 = Date.now() - let secondInstance = app.makeSingleInstance((argv) => { - app.window.focus() - }) - - if (secondInstance) { - app.quit() - return - } - let configPath = path.join(electron.app.getPath('userData'), 'config.yaml') let configData if (fs.existsSync(configPath)) { @@ -123,6 +138,7 @@ start = () => { //- background to avoid the flash of unstyled window backgroundColor: '#1D272D', frame: false, + type: 'toolbar', } Object.assign(options, windowConfig.get('windowBoundaries')) diff --git a/app/package.json b/app/package.json index 7f4fdb6d..6f6893fc 100644 --- a/app/package.json +++ b/app/package.json @@ -3,15 +3,15 @@ "version": "1.0.0", "main": "main.js", "dependencies": { - "child-process-promise": "^2.2.0", - "devtron": "^1.4.0", - "electron-config": "^0.2.1", - "electron-debug": "^1.0.1", - "electron-is-dev": "^0.1.2", - "path": "^0.12.7", - "pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642" + "child-process-promise": "2.2.0", + "devtron": "1.4.0", + "electron-config": "0.2.1", + "electron-debug": "1.0.1", + "electron-is-dev": "0.1.2", + "node-pty": "0.6.3", + "path": "0.12.7" }, "devDependencies": { - "js-yaml": "^3.8.2" + "js-yaml": "3.8.2" } } diff --git a/app/src/app.module.ts b/app/src/app.module.ts index 69a074ba..d1d110b0 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -16,7 +16,6 @@ import { PluginDispatcherService } from 'services/pluginDispatcher' import { QuitterService } from 'services/quitter' import { SessionsService } from 'services/sessions' import { DockingService } from 'services/docking' -import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' import { AppComponent } from 'components/app' import { CheckboxComponent } from 'components/checkbox' @@ -48,7 +47,6 @@ import { TerminalComponent } from 'components/terminal' PluginDispatcherService, QuitterService, SessionsService, - LocalStorageService, ], entryComponents: [ HotkeyInputModalComponent, diff --git a/app/src/components/app.less b/app/src/components/app.less index d1e66eb0..6a15a693 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -185,6 +185,7 @@ &.active { background: @title-bg; + box-shadow: 0px -1px 0px 0px blue; .content-wrapper { //border-bottom: 2px solid #69bbea; diff --git a/app/src/components/app.pug b/app/src/components/app.pug index 1e53b0b5..e018efdc 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -1,8 +1,8 @@ .titlebar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"') - .title((dblclick)='hostApp.maximizeWindow()') Term - button.btn.btn-secondary.btn-minimize((click)='hostApp.minimizeWindow()') + .title((dblclick)='hostApp.toggleMaximize()') Term + button.btn.btn-secondary.btn-minimize((click)='hostApp.minimize()') i.fa.fa-window-minimize - button.btn.btn-secondary.btn-maximize((click)='hostApp.maximizeWindow()') + button.btn.btn-secondary.btn-maximize((click)='hostApp.toggleMaximize()') i.fa.fa-window-maximize button.btn.btn-secondary.btn-close((click)='hostApp.quit()') i.fa.fa-close diff --git a/app/src/components/app.ts b/app/src/components/app.ts index bdea3361..21d8f050 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -66,11 +66,11 @@ export class AppComponent { private elementRef: ElementRef, private sessions: SessionsService, private docking: DockingService, + private electron: ElectronService, public hostApp: HostAppService, public hotkeys: HotkeysService, public config: ConfigService, log: LogService, - electron: ElectronService, _quitter: QuitterService, ) { console.timeStamp('AppComponent ctor') @@ -135,10 +135,33 @@ export class AppComponent { this.hostApp.shown.subscribe(() => { this.docking.dock() }) + + this.hostApp.secondInstance.subscribe(() => { + if (this.electron.app.window.isFocused()) { + // focused + this.electron.app.window.hide() + } else { + if (!this.electron.app.window.isVisible()) { + // unfocused, invisible + this.electron.app.window.show() + } else { + if (this.config.full().appearance.dock == 'off') { + // not docked, visible + setTimeout(() => { + this.electron.app.window.focus() + }) + } else { + // docked, visible + this.electron.app.window.hide() + } + } + } + this.docking.dock() + }) } newTab () { - this.addTerminalTab(this.sessions.createNewSession({command: 'bash'})) + this.addTerminalTab(this.sessions.createNewSession({shell: 'zsh'})) } addTerminalTab (session) { diff --git a/app/src/components/settingsPane.pug b/app/src/components/settingsPane.pug index 4109ca0f..1a278466 100644 --- a/app/src/components/settingsPane.pug +++ b/app/src/components/settingsPane.pug @@ -31,7 +31,7 @@ ngb-tabset(type='tabs') label Dock the terminal br .row - .col-auto + .col.col-auto div( '[(ngModel)]'='config.store.appearance.dock' '(ngModelChange)'='config.save(); docking.dock()' @@ -71,10 +71,10 @@ ngb-tabset(type='tabs') input( type='range', '[(ngModel)]'='config.store.appearance.dockFill', - '(ngModelChange)'='config.save(); docking.dock()', - min='1', - max='100', - step='1' + '(mouseup)'='config.save(); docking.dock()', + min='0.05', + max='1', + step='0.01' ) br div( diff --git a/app/src/components/terminal.less b/app/src/components/terminal.less deleted file mode 100644 index 306dd453..00000000 --- a/app/src/components/terminal.less +++ /dev/null @@ -1,5 +0,0 @@ -:host { - position: relative; - display: block; - overflow: hidden; -} diff --git a/app/src/components/terminal.scss b/app/src/components/terminal.scss new file mode 100644 index 00000000..b291db41 --- /dev/null +++ b/app/src/components/terminal.scss @@ -0,0 +1,10 @@ +:host { + position: relative; + display: block; + overflow: hidden; + + div[style]:last-child { + background: black !important; + color: white !important; + } +} diff --git a/app/src/components/terminal.ts b/app/src/components/terminal.ts index d9a8c8fe..58e69d31 100644 --- a/app/src/components/terminal.ts +++ b/app/src/components/terminal.ts @@ -25,19 +25,10 @@ hterm.hterm.VT.ESC['k'] = function(parseState) { hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() const preferenceManager = new hterm.hterm.PreferenceManager('default') preferenceManager.set('user-css', dataurl.convert({ - data: ` - a { - cursor: pointer; - } - - a:hover { - text-decoration: underline; - } - `, + data: require('./terminal.userCSS.scss'), mimetype: 'text/css', charset: 'utf8', })) -preferenceManager.set('font-size', 12) preferenceManager.set('background-color', '#1D272D') preferenceManager.set('color-palette-overrides', { 0: '#1D272D', @@ -49,11 +40,12 @@ hterm.hterm.ScrollPort.prototype.decorate = function (...args) { this.screen_.style.cssText += `; padding-right: ${this.screen_.offsetWidth - this.screen_.clientWidth}px;` } +hterm.hterm.Terminal.prototype.showOverlay = () => null @Component({ selector: 'terminal', template: '', - styles: [require('./terminal.less')], + styles: [require('./terminal.scss')], }) export class TerminalComponent { @Input() session: Session @@ -115,6 +107,7 @@ export class TerminalComponent { preferenceManager.set('font-size', config.appearance.fontSize) preferenceManager.set('audible-bell-sound', '') preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification') + preferenceManager.set('enable-clipboard-notice', false) } ngOnDestroy () { diff --git a/app/src/components/terminal.userCSS.scss b/app/src/components/terminal.userCSS.scss new file mode 100644 index 00000000..9c675e12 --- /dev/null +++ b/app/src/components/terminal.userCSS.scss @@ -0,0 +1,11 @@ +a { + cursor: pointer; +} + +a:hover { + text-decoration: underline; +} + +* { + font-feature-settings: "liga" 0; // disable ligatures (they break monospacing) +} diff --git a/app/src/services/config.ts b/app/src/services/config.ts index 0ab1689b..040f48fb 100644 --- a/app/src/services/config.ts +++ b/app/src/services/config.ts @@ -14,6 +14,7 @@ export interface IAppearanceData { fontSize: number dock: string dockScreen: string + dockFill: number } export interface ITerminalData { diff --git a/app/src/services/docking.ts b/app/src/services/docking.ts index cb342eb0..3bed2555 100644 --- a/app/src/services/docking.ts +++ b/app/src/services/docking.ts @@ -26,18 +26,19 @@ export class DockingService { let dockSide = this.config.full().appearance.dock let newBounds: Electron.Rectangle = { x: 0, y: 0, width: 0, height: 0 } - let fill = 0.5 + let fill = this.config.full().appearance.dockFill if (dockSide == 'off') { + this.hostApp.setAlwaysOnTop(false) return } if (dockSide == 'left' || dockSide == 'right') { - newBounds.width = fill * display.bounds.width + newBounds.width = Math.round(fill * display.bounds.width) newBounds.height = display.bounds.height } if (dockSide == 'top' || dockSide == 'bottom') { newBounds.width = display.bounds.width - newBounds.height = fill * display.bounds.height + newBounds.height = Math.round(fill * display.bounds.height) } if (dockSide == 'right') { newBounds.x = display.bounds.x + display.bounds.width * (1.0 - fill) @@ -50,6 +51,8 @@ export class DockingService { newBounds.y = display.bounds.y } + this.hostApp.setAlwaysOnTop(true) + this.hostApp.unmaximize() this.hostApp.setBounds(newBounds) } diff --git a/app/src/services/hostApp.ts b/app/src/services/hostApp.ts index d49f57a4..3a4201ab 100644 --- a/app/src/services/hostApp.ts +++ b/app/src/services/hostApp.ts @@ -23,10 +23,14 @@ export class HostAppService { console.error('Unhandled exception:', err) }) - electron.ipcRenderer.on('window-shown', () => { + electron.ipcRenderer.on('host:window-shown', () => { this.shown.emit() }) + electron.ipcRenderer.on('host:second-instance', () => { + this.secondInstance.emit() + }) + this.ready.subscribe(() => { electron.ipcRenderer.send('app:ready') }) @@ -36,6 +40,7 @@ export class HostAppService { quitRequested = new EventEmitter() ready = new EventEmitter() shown = new EventEmitter() + secondInstance = new EventEmitter() private logger: Logger; @@ -59,8 +64,8 @@ export class HostAppService { this.electron.app.webContents.openDevTools() } - setWindowCloseable(flag: boolean) { - this.electron.ipcRenderer.send('window-closeable', flag) + setCloseable(flag: boolean) { + this.electron.ipcRenderer.send('window-set-closeable', flag) } focusWindow() { @@ -71,18 +76,30 @@ export class HostAppService { this.electron.ipcRenderer.send('window-toggle-focus') } - minimizeWindow () { + minimize () { this.electron.ipcRenderer.send('window-minimize') } - maximizeWindow () { + maximize () { this.electron.ipcRenderer.send('window-maximize') } + unmaximize () { + this.electron.ipcRenderer.send('window-unmaximize') + } + + toggleMaximize () { + this.electron.ipcRenderer.send('window-toggle-maximize') + } + setBounds (bounds: Electron.Rectangle) { this.electron.ipcRenderer.send('window-set-bounds', bounds) } + setAlwaysOnTop (flag: boolean) { + this.electron.ipcRenderer.send('window-set-always-on-top', flag) + } + quit () { this.logger.info('Quitting') this.electron.app.quit() diff --git a/app/src/services/quitter.ts b/app/src/services/quitter.ts index ad2cb11c..644b38fd 100644 --- a/app/src/services/quitter.ts +++ b/app/src/services/quitter.ts @@ -13,7 +13,7 @@ export class QuitterService { } quit() { - this.hostApp.setWindowCloseable(true) + this.hostApp.setCloseable(true) this.hostApp.quit() } } diff --git a/app/src/services/sessions.ts b/app/src/services/sessions.ts index 0a508538..6023ab00 100644 --- a/app/src/services/sessions.ts +++ b/app/src/services/sessions.ts @@ -2,7 +2,8 @@ import { Injectable, NgZone, EventEmitter } from '@angular/core' import { Logger, LogService } from 'services/log' const exec = require('child-process-promise').exec import * as crypto from 'crypto' -import * as ptyjs from 'pty.js' +import * as nodePTY from 'node-pty' +import * as fs from 'fs' export interface SessionRecoveryProvider { @@ -42,16 +43,26 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider { getNewSessionCommand(command: string): string { const id = crypto.randomBytes(8).toString('hex') - return `screen -U -S term-tab-${id} -- ${command}` + // TODO + let configPath = '/tmp/.termScreenConfig' + fs.writeFileSync(configPath, ` + escape ^^^ + vbell off + term xterm-color + bindkey "^[OH" beginning-of-line + bindkey "^[OF" end-of-line + `, 'utf-8') + return `screen -c ${configPath} -U -S term-tab-${id} -- ${command}` } } export interface SessionOptions { name?: string, - command: string, + command?: string, + shell?: string, cwd?: string, - env?: string, + env?: any, } export class Session { @@ -67,14 +78,22 @@ export class Session { constructor (options: SessionOptions) { this.name = options.name console.log('Spawning', options.command) - this.pty = ptyjs.spawn('sh', ['-c', options.command], { - name: 'screen-256color', - //name: 'xterm-256color', + + let binary = options.shell || 'sh' + let args = options.shell ? [] : ['-c', options.command] + let env = { + ...process.env, + ...options.env, + TERM: 'xterm-256color', + } + this.pty = nodePTY.spawn(binary, args, { + //name: 'screen-256color', + name: 'xterm-256color', //name: 'xterm-color', cols: 80, rows: 30, cwd: options.cwd || process.env.HOME, - env: options.env || process.env, + env: env, }) this.open = true @@ -156,8 +175,8 @@ export class SessionsService { log: LogService, ) { this.logger = log.create('sessions') - //this.recoveryProvider = new ScreenSessionRecoveryProvider() - this.recoveryProvider = new NullSessionRecoveryProvider() + this.recoveryProvider = new ScreenSessionRecoveryProvider() + //this.recoveryProvider = new NullSessionRecoveryProvider() } createNewSession (options: SessionOptions) : Session { diff --git a/package.json b/package.json index 84d36481..0bcfd31c 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,12 @@ "apply-loader": "^0.1.0", "autoprefixer": "^6.7.7", "awesome-typescript-loader": "3.0.8", - "bootstrap": "^4.0.0-alpha.6", "css-loader": "0.26.1", "dataurl": "^0.1.0", - "electron": "^1.4.13", + "electron": "1.6.2", "electron-builder": "10.6.1", "electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04", - "electron-rebuild": "1.4.0", + "electron-rebuild": "1.5.7", "file-loader": "^0.9.0", "font-awesome": "4.7.0", "html-loader": "^0.4.4", @@ -69,14 +68,12 @@ "@angular/router": "3.3.1", "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.15", "@types/core-js": "^0.9.35", - "@types/electron": "^1.4.33", + "@types/electron": "1.4.34", "@types/js-yaml": "^3.5.29", "@types/node": "^7.0.5", - "@types/pty.js": "^0.2.32", - "angular2-localstorage": "github:AilisObrian/angular2-localstorage", "angular2-perfect-scrollbar": "^1.1.0", "angular2-toaster": "^1.1.0", - "bootstrap": "^3.3.7", + "bootstrap": "4.0.0-alpha.6", "core-js": "^2.4.1", "deepmerge": "^1.3.2", "hterm-commonjs": "^1.0.0", diff --git a/tsconfig.json b/tsconfig.json index fa4675ef..ab988487 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,12 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedParameters": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "lib": [ + "dom", + "es2015", + "es7" + ] }, "compileOnSave": false, "exclude": [ diff --git a/webpack.config.js b/webpack.config.js index 771a6573..aff189a0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const webpack = require("webpack") module.exports = { target: 'node', entry: { - 'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!./app/index.pug', + 'index.ignore': 'file-loader?name=index.html!pug-html-loader!./app/index.pug', 'preload': './app/src/entry.preload.ts', 'bundle': './app/src/entry.ts', }, @@ -52,7 +52,13 @@ module.exports = { }, { test: /\.scss$/, - use: ['style-loader', 'css-loader', 'sass-loader'] + use: ['style-loader', 'css-loader', 'sass-loader'], + exclude: [/app\/src\/components\//], + }, + { + test: /\.scss$/, + use: ['to-string-loader', 'css-loader', 'sass-loader'], + include: [/app\/src\/components\//], }, { test: /\.(png|svg)$/, @@ -83,7 +89,7 @@ module.exports = { 'shell': 'require("shell")', 'ipc': 'require("ipc")', 'crypto': 'require("crypto")', - 'pty.js': 'require("pty.js")', + 'node-pty': 'require("node-pty")', 'child-process-promise': 'require("child-process-promise")', }, plugins: [