From 5b76947d704afc9111f2539d42e984168e75a264 Mon Sep 17 00:00:00 2001 From: Domain Date: Wed, 29 Aug 2018 17:15:00 +0800 Subject: [PATCH 1/4] Add login scripts support (#402) * Support login scripts. Fix #344 * nowrap * Follow the general Terminus style --- terminus-ssh/src/api.ts | 52 ++++++- .../editConnectionModal.component.pug | 128 +++++++++++++----- .../editConnectionModal.component.ts | 41 +++++- .../components/sshSettingsTab.component.ts | 1 + terminus-ssh/src/services/ssh.service.ts | 2 +- 5 files changed, 183 insertions(+), 41 deletions(-) diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index 5edb4ea6..85f9cd46 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -1,5 +1,10 @@ import { BaseSession } from 'terminus-terminal' +export interface LoginScript { + expect?: string + send: string +} + export interface SSHConnection { name?: string host: string @@ -7,18 +12,43 @@ export interface SSHConnection { user: string password?: string privateKey?: string + scripts?: LoginScript[] } export class SSHSession extends BaseSession { - constructor (private shell: any) { + scripts?: LoginScript[] + + constructor (private shell: any, conn: SSHConnection) { super() + this.scripts = conn.scripts.slice(0); } start () { this.open = true this.shell.on('data', data => { - this.emitOutput(data.toString()) + let dataString = data.toString() + this.emitOutput(dataString) + + if (this.scripts && this.scripts.length > 0) { + let found = false + for (let i = 0; i < this.scripts.length; i++) { + if (dataString.indexOf(this.scripts[i].expect) >= 0) { + console.log("Executing: " + this.scripts[i].send) + this.shell.write(this.scripts[i].send + "\n") + this.scripts.splice(i, 1) + i-- + found = true + } + else { + break; + } + } + + if (found) { + this.executeScripts() + } + } }) this.shell.on('end', () => { @@ -26,6 +56,24 @@ export class SSHSession extends BaseSession { this.destroy() } }) + + this.executeScripts() + } + + executeScripts () { + if (this.scripts && this.scripts.length > 0) { + for (let i = 0; i < this.scripts.length; i++) { + if (!this.scripts[i].expect) { + console.log("Executing: " + this.scripts[i].send) + this.shell.write(this.scripts[i].send + "\n") + this.scripts.splice(i, 1) + i-- + } + else { + break; + } + } + } } resize (columns, rows) { diff --git a/terminus-ssh/src/components/editConnectionModal.component.pug b/terminus-ssh/src/components/editConnectionModal.component.pug index 006e7563..9fdbad7a 100644 --- a/terminus-ssh/src/components/editConnectionModal.component.pug +++ b/terminus-ssh/src/components/editConnectionModal.component.pug @@ -1,45 +1,101 @@ .modal-body - .form-group - label Name - input.form-control( - type='text', - [(ngModel)]='connection.name', - ) + ngb-tabset(type='pills', [activeId]='basic') + ngb-tab(id='basic') + ng-template(ngbTabTitle) + | Basic Setting + ng-template(ngbTabContent) + h4 Basic Setting + .form-group + label Name + input.form-control( + type='text', + [(ngModel)]='connection.name', + ) - .form-group - label Host - input.form-control( - type='text', - [(ngModel)]='connection.host', - ) + .form-group + label Host + input.form-control( + type='text', + [(ngModel)]='connection.host', + ) - .form-group - label Port - input.form-control( - type='number', - placeholder='22', - [(ngModel)]='connection.port', - ) + .form-group + label Port + input.form-control( + type='number', + placeholder='22', + [(ngModel)]='connection.port', + ) - .form-group - label Username - input.form-control( - type='text', - [(ngModel)]='connection.user', - ) + .form-group + label Username + input.form-control( + type='text', + [(ngModel)]='connection.user', + ) - .form-group - label Private key - .input-group - input.form-control( - type='text', - placeholder='Key file path', - [(ngModel)]='connection.privateKey' - ) - .input-group-btn - button.btn.btn-secondary((click)='selectPrivateKey()') - i.fa.fa-folder-open + .form-group + label Private key + .input-group + input.form-control( + type='text', + placeholder='Key file path', + [(ngModel)]='connection.privateKey' + ) + .input-group-btn + button.btn.btn-secondary((click)='selectPrivateKey()') + i.fa.fa-folder-open + ngb-tab(id='scripts') + ng-template(ngbTabTitle) + | Login Scripts + ng-template(ngbTabContent) + h4 Login Scripts + .list-group + table + tr + th String to wait + th String to be sent + th Actions + tr(*ngFor='let script of connection.scripts') + td + input.form-control( + type='text', + value='{{script.expect}}', + ) + td + input.form-control( + type='text', + value='{{script.send}}', + ) + td + .input-group.flex-nowrap + button.btn.btn-outline-info.ml-0((click)='up(script)') + i.fa.fa-arrow-up + button.btn.btn-outline-info.ml-0((click)='down(script)') + i.fa.fa-arrow-down + button.btn.btn-outline-danger.ml-0((click)='delete(script)') + i.fa.fa-trash-o + tr + td + input.form-control( + type='text', + placeholder='Enter a string to wait', + [(ngModel)]='newScript.expect' + ) + td + input.form-control( + type='text', + placeholder='Enter a string to be sent', + [(ngModel)]='newScript.send' + ) + td + .input-group.flex-nowrap + button.btn.btn-outline-info.ml-0((click)='add()') + i.fa.fa-save + button.btn.btn-outline-danger.ml-0((click)='clear()') + i.fa.fa-trash-o + .modal-footer button.btn.btn-outline-primary((click)='save()') Save button.btn.btn-outline-danger((click)='cancel()') Cancel diff --git a/terminus-ssh/src/components/editConnectionModal.component.ts b/terminus-ssh/src/components/editConnectionModal.component.ts index 62dd3eb1..d33b3053 100644 --- a/terminus-ssh/src/components/editConnectionModal.component.ts +++ b/terminus-ssh/src/components/editConnectionModal.component.ts @@ -1,19 +1,22 @@ import { Component } from '@angular/core' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { ElectronService, HostAppService } from 'terminus-core' -import { SSHConnection } from '../api' +import { SSHConnection, LoginScript } from '../api' @Component({ template: require('./editConnectionModal.component.pug'), }) export class EditConnectionModalComponent { connection: SSHConnection + newScript: LoginScript constructor ( private modalInstance: NgbActiveModal, private electron: ElectronService, private hostApp: HostAppService, - ) { } + ) { + this.newScript = { expect: "", send: ""} + } selectPrivateKey () { let path = this.electron.dialog.showOpenDialog( @@ -34,4 +37,38 @@ export class EditConnectionModalComponent { cancel () { this.modalInstance.dismiss() } + + up (script: LoginScript) { + let index = this.connection.scripts.indexOf(script) + if (index > 0) { + this.connection.scripts.splice(index, 1); + this.connection.scripts.splice(index - 1, 0, script); + } + } + + down (script: LoginScript) { + let index = this.connection.scripts.indexOf(script) + if (index >= 0 && index < this.connection.scripts.length - 1) { + this.connection.scripts.splice(index, 1); + this.connection.scripts.splice(index + 1, 0, script); + } + } + + delete (script: LoginScript) { + if (confirm(`Delete?`)) { + this.connection.scripts = this.connection.scripts.filter(x => x !== script) + } + } + + add () { + if (!this.connection.scripts) + this.connection.scripts = [] + this.connection.scripts.push(Object.assign({}, this.newScript)) + this.clear(); + } + + clear () { + this.newScript.expect = "" + this.newScript.send = "" + } } diff --git a/terminus-ssh/src/components/sshSettingsTab.component.ts b/terminus-ssh/src/components/sshSettingsTab.component.ts index 020f6a9a..2e13d0dd 100644 --- a/terminus-ssh/src/components/sshSettingsTab.component.ts +++ b/terminus-ssh/src/components/sshSettingsTab.component.ts @@ -24,6 +24,7 @@ export class SSHSettingsTabComponent { port: 22, user: 'root', } + let modal = this.ngbModal.open(EditConnectionModalComponent) modal.componentInstance.connection = connection modal.result.then(result => { diff --git a/terminus-ssh/src/services/ssh.service.ts b/terminus-ssh/src/services/ssh.service.ts index 04aae782..afebc65a 100644 --- a/terminus-ssh/src/services/ssh.service.ts +++ b/terminus-ssh/src/services/ssh.service.ts @@ -148,7 +148,7 @@ export class SSHService { }) }) - let session = new SSHSession(shell) + let session = new SSHSession(shell, connection) return this.zone.run(() => this.app.openNewTab( TerminalTabComponent, From 0749096d9f998ce57f8c1878f162748fea3097e7 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 31 Aug 2018 12:41:58 +0200 Subject: [PATCH 2/4] smarter progress detection (fixes #406) --- terminus-core/src/components/baseTab.component.ts | 10 ++++++++++ terminus-core/src/components/tabHeader.component.scss | 2 +- .../src/components/terminalTab.component.ts | 10 ++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts index d236cce3..c9993895 100644 --- a/terminus-core/src/components/baseTab.component.ts +++ b/terminus-core/src/components/baseTab.component.ts @@ -15,6 +15,8 @@ export abstract class BaseTabComponent { protected progress = new Subject() protected activity = new Subject() + private progressClearTimeout: number + get focused$ (): Observable { return this.focused } get blurred$ (): Observable { return this.blurred } get titleChange$ (): Observable { return this.titleChange } @@ -40,6 +42,14 @@ export abstract class BaseTabComponent { setProgress (progress: number) { this.progress.next(progress) + if (progress) { + if (this.progressClearTimeout) { + clearTimeout(this.progressClearTimeout) + } + this.progressClearTimeout = setTimeout(() => { + this.setProgress(null) + }, 5000) + } } displayActivity (): void { diff --git a/terminus-core/src/components/tabHeader.component.scss b/terminus-core/src/components/tabHeader.component.scss index 6a1ea431..963a9112 100644 --- a/terminus-core/src/components/tabHeader.component.scss +++ b/terminus-core/src/components/tabHeader.component.scss @@ -79,7 +79,7 @@ $tabs-height: 36px; position: absolute; left: 0; top: 0; - bottom: 0; + height: 5px; z-index: -1; } } diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index fd3aac84..eedf4050 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -295,11 +295,13 @@ export class TerminalTabComponent extends BaseTabComponent { } write (data: string) { - let percentageMatch = /(\d+(\.\d+)?)%/.exec(data) + let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data) if (percentageMatch) { - let percentage = percentageMatch[2] ? parseFloat(percentageMatch[1]) : parseInt(percentageMatch[1]) - this.setProgress(percentage) - console.log('Detected progress:', percentage) + let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2]) + if (percentage > 0 && percentage <= 100) { + this.setProgress(percentage) + console.log('Detected progress:', percentage) + } } else { this.setProgress(null) } From 4b7b692ace3c5d724b4affef999becb710eb1ac9 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 31 Aug 2018 15:41:28 +0200 Subject: [PATCH 3/4] experimental support for multiple windows (fixes #212, fixes #170) --- app/lib/app.ts | 171 ++++++++++ app/lib/cli.js | 23 -- app/lib/cli.ts | 23 ++ app/lib/index.js | 304 ------------------ app/lib/index.ts | 59 ++++ app/lib/{lru.js => lru.ts} | 10 +- app/lib/window.ts | 173 ++++++++++ app/src/entry.preload.ts | 4 +- app/tsconfig.main.json | 31 ++ app/webpack.config.js | 40 +-- app/webpack.main.config.js | 9 +- package.json | 1 + .../src/components/appRoot.component.ts | 22 +- terminus-core/src/configDefaults.linux.yaml | 2 + terminus-core/src/configDefaults.macos.yaml | 2 + terminus-core/src/configDefaults.windows.yaml | 2 + terminus-core/src/services/app.service.ts | 22 +- terminus-core/src/services/config.service.ts | 8 +- terminus-core/src/services/docking.service.ts | 8 +- .../src/services/electron.service.ts | 4 +- terminus-core/src/services/hostApp.service.ts | 35 +- terminus-core/src/services/hotkeys.service.ts | 4 + .../src/services/touchbar.service.ts | 4 +- terminus-terminal/src/buttonProvider.ts | 7 +- yarn.lock | 4 + 25 files changed, 577 insertions(+), 395 deletions(-) create mode 100644 app/lib/app.ts delete mode 100644 app/lib/cli.js create mode 100644 app/lib/cli.ts delete mode 100644 app/lib/index.js create mode 100644 app/lib/index.ts rename app/lib/{lru.js => lru.ts} (76%) create mode 100644 app/lib/window.ts create mode 100644 app/tsconfig.main.json diff --git a/app/lib/app.ts b/app/lib/app.ts new file mode 100644 index 00000000..9c38c460 --- /dev/null +++ b/app/lib/app.ts @@ -0,0 +1,171 @@ +import { app, ipcMain, Menu, Tray, shell } from 'electron' +import { Window } from './window' + +export class Application { + private tray: Tray + private windows: Window[] = [] + + constructor () { + ipcMain.on('app:config-change', () => { + this.broadcast('host:config-change') + }) + } + + async newWindow (): Promise { + let window = new Window() + this.windows.push(window) + window.visible$.subscribe(visible => { + if (visible) { + this.disableTray() + } else { + this.enableTray() + } + }) + this.setupMenu() + await window.ready + return window + } + + broadcast (event, ...args) { + for (let window of this.windows) { + window.send(event, ...args) + } + } + + async send (event, ...args) { + if (!this.hasWindows()) { + await this.newWindow() + } + this.windows[0].send(event, ...args) + } + + enableTray () { + if (this.tray) { + return + } + if (process.platform === 'darwin') { + this.tray = new Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`) + this.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`) + } else { + this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`) + } + + this.tray.on('click', () => this.focus()) + + const contextMenu = Menu.buildFromTemplate([{ + label: 'Show', + click: () => this.focus(), + }]) + + if (process.platform !== 'darwin') { + this.tray.setContextMenu(contextMenu) + } + + this.tray.setToolTip(`Terminus ${app.getVersion()}`) + } + + disableTray () { + if (this.tray) { + this.tray.destroy() + this.tray = null + } + } + + hasWindows () { + return !!this.windows.length + } + + focus () { + for (let window of this.windows) { + window.show() + window.focus() + } + } + + private setupMenu () { + let template: Electron.MenuItemConstructorOptions[] = [ + { + label: 'Application', + submenu: [ + { role: 'about', label: 'About Terminus' }, + { type: 'separator' }, + { + label: 'Preferences', + accelerator: 'Cmd+,', + async click () { + if (!this.hasWindows()) { + await this.newWindow() + } + this.windows[0].send('host:preferences-menu') + }, + }, + { type: 'separator' }, + { role: 'services', submenu: [] }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { + label: 'Quit', + accelerator: 'Cmd+Q', + click () { + app.quit() + }, + }, + ], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'delete' }, + { role: 'selectall' }, + ], + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin' }, + { role: 'zoomout' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { + role: 'window', + submenu: [ + { role: 'minimize' }, + { role: 'close' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' }, + ], + }, + { + role: 'help', + submenu: [ + { + label: 'Website', + click () { + shell.openExternal('https://eugeny.github.io/terminus') + }, + }, + ], + } + ] + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)) + } +} diff --git a/app/lib/cli.js b/app/lib/cli.js deleted file mode 100644 index 53d5d541..00000000 --- a/app/lib/cli.js +++ /dev/null @@ -1,23 +0,0 @@ -import { app } from 'electron' - -export function parseArgs (argv, cwd) { - if (argv[0].includes('node')) { - argv = argv.slice(1) - } - - return require('yargs') - .usage('terminus [command] [arguments]') - .command('open [directory]', 'open a shell in a directory', { - directory: { type: 'string', 'default': cwd }, - }) - .command('run [command...]', 'run a command in the terminal', { - command: { type: 'string' }, - }) - .version('v', 'Show version and exit', app.getVersion()) - .alias('d', 'debug') - .describe('d', 'Show DevTools on start') - .alias('h', 'help') - .help('h') - .strict() - .parse(argv.slice(1)) -} diff --git a/app/lib/cli.ts b/app/lib/cli.ts new file mode 100644 index 00000000..c5f36643 --- /dev/null +++ b/app/lib/cli.ts @@ -0,0 +1,23 @@ +import { app } from 'electron' + +export function parseArgs (argv, cwd) { + if (argv[0].includes('node')) { + argv = argv.slice(1) + } + + return require('yargs') + .usage('terminus [command] [arguments]') + .command('open [directory]', 'open a shell in a directory', { + directory: { type: 'string', 'default': cwd }, + }) + .command('run [command...]', 'run a command in the terminal', { + command: { type: 'string' }, + }) + .version('v', 'Show version and exit', app.getVersion()) + .alias('d', 'debug') + .describe('d', 'Show DevTools on start') + .alias('h', 'help') + .help('h') + .strict() + .parse(argv.slice(1)) +} diff --git a/app/lib/index.js b/app/lib/index.js deleted file mode 100644 index cd072c1a..00000000 --- a/app/lib/index.js +++ /dev/null @@ -1,304 +0,0 @@ -import { app, ipcMain, BrowserWindow, Menu, Tray, shell } from 'electron' -import * as path from 'path' -import electronDebug from 'electron-debug' -import * as fs from 'fs' -import * as yaml from 'js-yaml' -import './lru' -import { parseArgs } from './cli' -import ElectronConfig from 'electron-config' -if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0) - -let electronVibrancy -if (process.platform !== 'linux') { - electronVibrancy = require('electron-vibrancy') -} - -let windowConfig = new ElectronConfig({ name: 'window' }) - -if (!process.env.TERMINUS_PLUGINS) { - process.env.TERMINUS_PLUGINS = '' -} - -const setWindowVibrancy = (enabled) => { - if (enabled && !app.window.vibrancyViewID) { - app.window.vibrancyViewID = electronVibrancy.SetVibrancy(app.window, 0) - } else if (!enabled && app.window.vibrancyViewID) { - electronVibrancy.RemoveView(app.window, app.window.vibrancyViewID) - app.window.vibrancyViewID = null - } -} - -const setupTray = () => { - if (process.platform === 'darwin') { - app.tray = new Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`) - app.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`) - } else { - app.tray = new Tray(`${app.getAppPath()}/assets/tray.png`) - } - - app.tray.on('click', () => { - app.window.show() - app.window.focus() - }) - - const contextMenu = Menu.buildFromTemplate([{ - label: 'Show', - click () { - app.window.show() - app.window.focus() - }, - }]) - - if (process.platform !== 'darwin') { - app.tray.setContextMenu(contextMenu) - } - - app.tray.setToolTip(`Terminus ${app.getVersion()}`) -} - -const setupWindowManagement = () => { - app.window.on('show', () => { - app.window.webContents.send('host:window-shown') - if (app.tray) { - app.tray.destroy() - app.tray = null - } - }) - - app.window.on('hide', () => { - if (!app.tray) { - setupTray() - } - }) - - app.window.on('enter-full-screen', () => app.window.webContents.send('host:window-enter-full-screen')) - app.window.on('leave-full-screen', () => app.window.webContents.send('host:window-leave-full-screen')) - - app.window.on('close', () => { - windowConfig.set('windowBoundaries', app.window.getBounds()) - }) - - app.window.on('closed', () => { - app.window = null - }) - - ipcMain.on('window-focus', () => { - app.window.focus() - }) - - ipcMain.on('window-maximize', () => { - app.window.maximize() - }) - - ipcMain.on('window-unmaximize', () => { - app.window.unmaximize() - }) - - ipcMain.on('window-toggle-maximize', () => { - if (app.window.isMaximized()) { - app.window.unmaximize() - } else { - app.window.maximize() - } - }) - - ipcMain.on('window-minimize', () => { - app.window.minimize() - }) - - ipcMain.on('window-set-bounds', (event, bounds) => { - app.window.setBounds(bounds) - }) - - ipcMain.on('window-set-always-on-top', (event, flag) => { - app.window.setAlwaysOnTop(flag) - }) - - ipcMain.on('window-set-vibrancy', (event, enabled) => { - setWindowVibrancy(enabled) - }) -} - -const setupMenu = () => { - let template = [{ - label: 'Application', - submenu: [ - { role: 'about', label: 'About Terminus' }, - { type: 'separator' }, - { - label: 'Preferences', - accelerator: 'Cmd+,', - click () { - app.window.webContents.send('host:preferences-menu') - }, - }, - { type: 'separator' }, - { role: 'services', submenu: [] }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { - label: 'Quit', - accelerator: 'Cmd+Q', - click () { - app.quit() - }, - }, - ], - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' }, - ], - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { role: 'forcereload' }, - { role: 'toggledevtools' }, - { type: 'separator' }, - { role: 'resetzoom' }, - { role: 'zoomin' }, - { role: 'zoomout' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ], - }, - { - role: 'window', - submenu: [ - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' }, - ], - }, - { - role: 'help', - submenu: [ - { - label: 'Website', - click () { - shell.openExternal('https://eugeny.github.io/terminus') - }, - }, - ], - }] - - Menu.setApplicationMenu(Menu.buildFromTemplate(template)) -} - -const start = () => { - let t0 = Date.now() - - let configPath = path.join(app.getPath('userData'), 'config.yaml') - let configData - if (fs.existsSync(configPath)) { - configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8')) - } else { - configData = {} - } - - let options = { - width: 800, - height: 600, - title: 'Terminus', - minWidth: 400, - minHeight: 300, - webPreferences: { webSecurity: false }, - frame: false, - show: false, - } - Object.assign(options, windowConfig.get('windowBoundaries')) - - if ((configData.appearance || {}).frame === 'native') { - options.frame = true - } else { - if (process.platform === 'darwin') { - options.titleBarStyle = 'hiddenInset' - } - } - - if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) { - options.transparent = true - } - - if (process.platform === 'linux') { - options.backgroundColor = '#131d27' - } - - app.commandLine.appendSwitch('disable-http-cache') - - app.window = new BrowserWindow(options) - app.window.once('ready-to-show', () => { - if (process.platform === 'darwin') { - app.window.setVibrancy('dark') - } else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) { - setWindowVibrancy(true) - } - app.window.show() - app.window.focus() - }) - app.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, { extraHeaders: 'pragma: no-cache\n' }) - - if (process.platform !== 'darwin') { - app.window.setMenu(null) - } - - setupWindowManagement() - - if (process.platform === 'darwin') { - setupMenu() - } else { - app.window.setMenu(null) - } - - console.info(`Host startup: ${Date.now() - t0}ms`) - t0 = Date.now() - ipcMain.on('app:ready', () => { - console.info(`App startup: ${Date.now() - t0}ms`) - }) -} - -app.on('activate', () => { - if (!app.window) { - start() - } else { - app.window.show() - app.window.focus() - } -}) - -process.on('uncaughtException', function (err) { - console.log(err) - app.window.webContents.send('uncaughtException', err) -}) - -app.on('second-instance', (event, argv, cwd) => { - app.window.webContents.send('host:second-instance', parseArgs(argv, cwd)) -}) - -const argv = parseArgs(process.argv, process.cwd()) - -if (!app.requestSingleInstanceLock()) { - app.quit() - process.exit(0) -} - -if (argv.d) { - electronDebug({ enabled: true, showDevTools: 'undocked' }) -} - -app.on('ready', start) diff --git a/app/lib/index.ts b/app/lib/index.ts new file mode 100644 index 00000000..91de8a9b --- /dev/null +++ b/app/lib/index.ts @@ -0,0 +1,59 @@ +import './lru' +import { app, ipcMain, Menu } from 'electron' +import electronDebug = require('electron-debug') +import { parseArgs } from './cli' +import { Application } from './app' +if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0) + +if (!process.env.TERMINUS_PLUGINS) { + process.env.TERMINUS_PLUGINS = '' +} + +const application = new Application() + +app.commandLine.appendSwitch('disable-http-cache') + +ipcMain.on('app:new-window', () => { + console.log('new-window') + application.newWindow() +}) + +app.on('activate', () => { + if (!application.hasWindows()) { + application.newWindow() + } else { + application.focus() + } +}) + +process.on('uncaughtException' as any, err => { + console.log(err) + application.broadcast('uncaughtException', err) +}) + +app.on('second-instance', (_event, argv, cwd) => { + application.send('host:second-instance', parseArgs(argv, cwd)) +}) + +const argv = parseArgs(process.argv, process.cwd()) + +if (!app.requestSingleInstanceLock()) { + app.quit() + process.exit(0) +} + +if (argv.d) { + electronDebug({ enabled: true, showDevTools: 'undocked' }) +} + +app.on('ready', () => { + app.dock.setMenu(Menu.buildFromTemplate([ + { + label: 'New window', + click () { + this.app.newWindow() + } + } + ])) + application.newWindow() +}) diff --git a/app/lib/lru.js b/app/lib/lru.ts similarity index 76% rename from app/lib/lru.js rename to app/lib/lru.ts index e3ffefbe..695db686 100644 --- a/app/lib/lru.js +++ b/app/lib/lru.ts @@ -6,10 +6,10 @@ let origLstat = fs.realpathSync.bind(fs) // NB: The biggest offender of thrashing realpathSync is the node module system // itself, which we can't get into via any sane means. require('fs').realpathSync = function (p) { - let r = lru.get(p) - if (r) return r + let r = lru.get(p) + if (r) return r - r = origLstat(p) - lru.set(p, r) - return r + r = origLstat(p) + lru.set(p, r) + return r } diff --git a/app/lib/window.ts b/app/lib/window.ts new file mode 100644 index 00000000..a33ed98f --- /dev/null +++ b/app/lib/window.ts @@ -0,0 +1,173 @@ +import { Subject, Observable } from 'rxjs' +import { BrowserWindow, app, ipcMain } from 'electron' +import ElectronConfig = require('electron-config') +import * as yaml from 'js-yaml' +import * as fs from 'fs' +import * as path from 'path' + +let electronVibrancy: any +if (process.platform !== 'linux') { + electronVibrancy = require('electron-vibrancy') +} + +export class Window { + ready: Promise + private visible = new Subject() + private window: BrowserWindow + private vibrancyViewID: number + private windowConfig: ElectronConfig + + get visible$ (): Observable { return this.visible } + + constructor () { + let configPath = path.join(app.getPath('userData'), 'config.yaml') + let configData + if (fs.existsSync(configPath)) { + configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8')) + } else { + configData = {} + } + + this.windowConfig = new ElectronConfig({ name: 'window' }) + + let options: Electron.BrowserWindowConstructorOptions = { + width: 800, + height: 600, + title: 'Terminus', + minWidth: 400, + minHeight: 300, + webPreferences: { webSecurity: false }, + frame: false, + show: false, + } + Object.assign(options, this.windowConfig.get('windowBoundaries')) + + if ((configData.appearance || {}).frame === 'native') { + options.frame = true + } else { + if (process.platform === 'darwin') { + options.titleBarStyle = 'hiddenInset' + } + } + + if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) { + options.transparent = true + } + + if (process.platform === 'linux') { + options.backgroundColor = '#131d27' + } + + this.window = new BrowserWindow(options) + this.window.once('ready-to-show', () => { + if (process.platform === 'darwin') { + this.window.setVibrancy('dark') + } else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) { + this.setVibrancy(true) + } + this.window.show() + this.window.focus() + }) + this.window.loadURL(`file://${app.getAppPath()}/dist/index.html?${this.window.id}`, { extraHeaders: 'pragma: no-cache\n' }) + + if (process.platform !== 'darwin') { + this.window.setMenu(null) + } + + this.setupWindowManagement() + + this.ready = new Promise(resolve => { + const listener = event => { + if (event.sender === this.window.webContents) { + ipcMain.removeListener('app:ready', listener) + resolve() + } + } + ipcMain.on('app:ready', listener) + }) + } + + setVibrancy (enabled: boolean) { + if (enabled && !this.vibrancyViewID) { + this.vibrancyViewID = electronVibrancy.SetVibrancy(this.window, 0) + } else if (!enabled && this.vibrancyViewID) { + electronVibrancy.RemoveView(this.window, this.vibrancyViewID) + this.vibrancyViewID = null + } + } + + show () { + this.window.show() + } + + focus () { + this.window.focus() + } + + send (event, ...args) { + this.window.webContents.send(event, ...args) + } + + private setupWindowManagement () { + this.window.on('show', () => { + this.visible.next(true) + this.window.webContents.send('host:window-shown') + }) + + this.window.on('hide', () => { + this.visible.next(false) + }) + + this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen')) + this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen')) + + this.window.on('close', () => { + this.windowConfig.set('windowBoundaries', this.window.getBounds()) + }) + + this.window.on('closed', () => { + this.destroy() + }) + + ipcMain.on('window-focus', () => { + this.window.focus() + }) + + ipcMain.on('window-maximize', () => { + this.window.maximize() + }) + + ipcMain.on('window-unmaximize', () => { + this.window.unmaximize() + }) + + ipcMain.on('window-toggle-maximize', () => { + if (this.window.isMaximized()) { + this.window.unmaximize() + } else { + this.window.maximize() + } + }) + + ipcMain.on('window-minimize', () => { + this.window.minimize() + }) + + ipcMain.on('window-set-bounds', (_event, bounds) => { + this.window.setBounds(bounds) + }) + + ipcMain.on('window-set-always-on-top', (_event, flag) => { + this.window.setAlwaysOnTop(flag) + }) + + ipcMain.on('window-set-vibrancy', (_event, enabled) => { + this.setVibrancy(enabled) + }) + } + + private destroy () { + this.window = null + this.visible.complete() + } +} diff --git a/app/src/entry.preload.ts b/app/src/entry.preload.ts index b415e078..2bd8b06d 100644 --- a/app/src/entry.preload.ts +++ b/app/src/entry.preload.ts @@ -1,4 +1,4 @@ -import '../lib/lru.js' +import '../lib/lru' import 'source-sans-pro' import 'font-awesome/css/font-awesome.css' import 'ngx-toastr/toastr.css' @@ -29,7 +29,7 @@ Raven.config( } ) -process.on('uncaughtException', (err) => { +process.on('uncaughtException' as any, (err) => { Raven.captureException(err) console.error(err) }) diff --git a/app/tsconfig.main.json b/app/tsconfig.main.json new file mode 100644 index 00000000..4c0b09db --- /dev/null +++ b/app/tsconfig.main.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "baseUrl": "./lib", + "module": "commonjs", + "target": "es2017", + "declaration": false, + "noImplicitAny": false, + "removeComments": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "lib": [ + "dom", + "es2015", + "es2015.iterable", + "es2017", + "es7" + ] + }, + "compileOnSave": false, + "exclude": [ + "dist", + "node_modules", + "*/node_modules" + ] +} diff --git a/app/webpack.config.js b/app/webpack.config.js index 1125fb57..b2532b23 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -6,8 +6,8 @@ module.exports = { target: 'node', entry: { 'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!' + path.resolve(__dirname, './index.pug'), - 'preload': path.resolve(__dirname, 'src/entry.preload.ts'), - 'bundle': path.resolve(__dirname, 'src/entry.ts'), + preload: path.resolve(__dirname, 'src/entry.preload.ts'), + bundle: path.resolve(__dirname, 'src/entry.ts'), }, mode: process.env.DEV ? 'development' : 'production', context: __dirname, @@ -15,7 +15,7 @@ module.exports = { output: { path: path.join(__dirname, 'dist'), pathinfo: true, - filename: '[name].js' + filename: '[name].js', }, resolve: { modules: ['src/', 'node_modules', '../node_modules', 'assets/'].map(x => path.join(__dirname, x)), @@ -29,8 +29,8 @@ module.exports = { loader: 'awesome-typescript-loader', options: { configFileName: path.resolve(__dirname, 'tsconfig.json'), - } - } + }, + }, }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, @@ -39,20 +39,20 @@ module.exports = { use: { loader: 'file-loader', options: { - name: 'images/[name].[ext]' - } - } + name: 'images/[name].[ext]', + }, + }, }, { test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: { loader: 'file-loader', options: { - name: 'fonts/[name].[ext]' - } - } - } - ] + name: 'fonts/[name].[ext]', + }, + }, + }, + ], }, externals: { '@angular/core': 'commonjs @angular/core', @@ -62,15 +62,15 @@ module.exports = { '@angular/forms': 'commonjs @angular/forms', '@angular/common': 'commonjs @angular/common', '@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap', - 'child_process': 'commonjs child_process', - 'electron': 'commonjs electron', + child_process: 'commonjs child_process', + electron: 'commonjs electron', 'electron-is-dev': 'commonjs electron-is-dev', - 'fs': 'commonjs fs', + fs: 'commonjs fs', 'ngx-toastr': 'commonjs ngx-toastr', - 'module': 'commonjs module', - 'mz': 'commonjs mz', - 'path': 'commonjs path', - 'rxjs': 'commonjs rxjs', + module: 'commonjs module', + mz: 'commonjs mz', + path: 'commonjs path', + rxjs: 'commonjs rxjs', 'zone.js': 'commonjs zone.js/dist/zone.js', }, plugins: [ diff --git a/app/webpack.main.config.js b/app/webpack.main.config.js index dcc33770..d392cd97 100644 --- a/app/webpack.main.config.js +++ b/app/webpack.main.config.js @@ -5,7 +5,7 @@ module.exports = { name: 'terminus-main', target: 'node', entry: { - main: path.resolve(__dirname, 'lib/index.js'), + main: path.resolve(__dirname, 'lib/index.ts'), }, mode: process.env.DEV ? 'development' : 'production', context: __dirname, @@ -22,12 +22,11 @@ module.exports = { module: { rules: [ { - test: /lib[\\/].*\.js$/, - exclude: /node_modules/, + test: /\.ts$/, use: { - loader: 'babel-loader', + loader: 'awesome-typescript-loader', options: { - presets: ['babel-preset-es2015'], + configFileName: path.resolve(__dirname, 'tsconfig.main.json'), }, }, }, diff --git a/package.json b/package.json index 35d5e0dd..16e7b0e9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@types/electron-config": "^0.2.1", "@types/electron-debug": "^1.1.0", "@types/fs-promise": "1.0.1", + "@types/js-yaml": "^3.11.2", "@types/node": "7.0.5", "@types/webpack-env": "1.13.0", "apply-loader": "0.1.0", diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index e1c41fbb..a026c291 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -142,9 +142,9 @@ export class AppRootComponent { this.unsortedTabs.push(tab) tab.progress$.subscribe(progress => { if (progress !== null) { - this.hostApp.getWindow().setProgressBar(progress / 100.0, 'normal') + this.hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' }) } else { - this.hostApp.getWindow().setProgressBar(-1, 'none') + this.hostApp.getWindow().setProgressBar(-1, { mode: 'none' }) } }) }) @@ -154,26 +154,26 @@ export class AppRootComponent { } onGlobalHotkey () { - if (this.electron.app.window.isFocused()) { + if (this.hostApp.getWindow().isFocused()) { // focused this.electron.loseFocus() if (this.hostApp.platform !== Platform.macOS) { - this.electron.app.window.hide() + this.hostApp.getWindow().hide() } } else { - if (!this.electron.app.window.isVisible()) { + if (!this.hostApp.getWindow().isVisible()) { // unfocused, invisible - this.electron.app.window.show() - this.electron.app.window.focus() + this.hostApp.getWindow().show() + this.hostApp.getWindow().focus() } else { if (this.config.store.appearance.dock === 'off') { // not docked, visible setTimeout(() => { - this.electron.app.window.focus() + this.hostApp.getWindow().focus() }) } else { // docked, visible - this.electron.app.window.hide() + this.hostApp.getWindow().hide() } } } @@ -223,7 +223,7 @@ export class AppRootComponent { } private updateVibrancy () { - this.hostApp.setVibrancy(this.config.store.appearance.vibrancy) - this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity) + this.hostApp.setVibrancy(this.config.store.appearance.vibrancy) + this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity) } } diff --git a/terminus-core/src/configDefaults.linux.yaml b/terminus-core/src/configDefaults.linux.yaml index 01042767..75671c35 100644 --- a/terminus-core/src/configDefaults.linux.yaml +++ b/terminus-core/src/configDefaults.linux.yaml @@ -1,4 +1,6 @@ hotkeys: + new-window: + - 'Ctrl-Shift-N' toggle-window: - 'Ctrl+Space' toggle-fullscreen: diff --git a/terminus-core/src/configDefaults.macos.yaml b/terminus-core/src/configDefaults.macos.yaml index a37ef6a9..239f30fc 100644 --- a/terminus-core/src/configDefaults.macos.yaml +++ b/terminus-core/src/configDefaults.macos.yaml @@ -1,4 +1,6 @@ hotkeys: + new-window: + - '⌘-N' toggle-window: - 'Ctrl+Space' toggle-fullscreen: diff --git a/terminus-core/src/configDefaults.windows.yaml b/terminus-core/src/configDefaults.windows.yaml index 7b0fbf9c..b5222d65 100644 --- a/terminus-core/src/configDefaults.windows.yaml +++ b/terminus-core/src/configDefaults.windows.yaml @@ -1,4 +1,6 @@ hotkeys: + new-window: + - 'Ctrl-Shift-N' toggle-window: - 'Ctrl+Space' toggle-fullscreen: diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index e08a509f..ef2b69e0 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -3,6 +3,7 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core' import { BaseTabComponent } from '../components/baseTab.component' import { Logger, LogService } from './log.service' import { ConfigService } from './config.service' +import { HostAppService } from './hostApp.service' export declare type TabComponentType = new (...args: any[]) => BaseTabComponent @@ -28,6 +29,7 @@ export class AppService { constructor ( private componentFactoryResolver: ComponentFactoryResolver, private config: ConfigService, + private hostApp: HostAppService, private injector: Injector, log: LogService, ) { @@ -37,15 +39,21 @@ export class AppService { openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent { let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type) let componentRef = componentFactory.create(this.injector) - componentRef.instance.hostView = componentRef.hostView - Object.assign(componentRef.instance, inputs || {}) + let tab = componentRef.instance + tab.hostView = componentRef.hostView + Object.assign(tab, inputs || {}) - this.tabs.push(componentRef.instance) - this.selectTab(componentRef.instance) + this.tabs.push(tab) + this.selectTab(tab) this.tabsChanged.next() - this.tabOpened.next(componentRef.instance) + this.tabOpened.next(tab) - return componentRef.instance + tab.titleChange$.subscribe(title => { + if (tab === this.activeTab) { + this.hostApp.getWindow().setTitle(title) + } + }) + return tab } selectTab (tab: BaseTabComponent) { @@ -67,6 +75,7 @@ export class AppService { if (this.activeTab) { this.activeTab.emitFocused() } + this.hostApp.getWindow().setTitle(this.activeTab.title) } toggleLastTab () { @@ -122,5 +131,6 @@ export class AppService { emitReady () { this.ready.next(null) this.ready.complete() + this.hostApp.emitReady() } } diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index 066c8f4e..e6433bd0 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -63,7 +63,7 @@ export class ConfigService { constructor ( electron: ElectronService, - hostApp: HostAppService, + private hostApp: HostAppService, @Inject(ConfigProvider) configProviders: ConfigProvider[], ) { this.path = path.join(electron.app.getPath('userData'), 'config.yaml') @@ -78,6 +78,11 @@ export class ConfigService { return defaults }).reduce(configMerge) this.load() + + hostApp.configChangeBroadcast$.subscribe(() => { + this.load() + this.emitChange() + }) } getDefaults () { @@ -96,6 +101,7 @@ export class ConfigService { save (): void { fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8') this.emitChange() + this.hostApp.broadcastConfigChange() } readRaw (): string { diff --git a/terminus-core/src/services/docking.service.ts b/terminus-core/src/services/docking.service.ts index 63c0ccce..018522f7 100644 --- a/terminus-core/src/services/docking.service.ts +++ b/terminus-core/src/services/docking.service.ts @@ -76,12 +76,8 @@ export class DockingService { }) } - getWindow () { - return this.electron.app.window - } - repositionWindow () { - let [x, y] = this.getWindow().getPosition() + let [x, y] = this.hostApp.getWindow().getPosition() for (let screen of this.electron.screen.getAllDisplays()) { let bounds = screen.bounds if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) { @@ -89,6 +85,6 @@ export class DockingService { } } let screen = this.electron.screen.getPrimaryDisplay() - this.getWindow().setPosition(screen.bounds.x, screen.bounds.y) + this.hostApp.getWindow().setPosition(screen.bounds.x, screen.bounds.y) } } diff --git a/terminus-core/src/services/electron.service.ts b/terminus-core/src/services/electron.service.ts index ccc238b3..d20e3bcc 100644 --- a/terminus-core/src/services/electron.service.ts +++ b/terminus-core/src/services/electron.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { TouchBar } from 'electron' +import { TouchBar, BrowserWindow } from 'electron' @Injectable() export class ElectronService { @@ -13,6 +13,7 @@ export class ElectronService { screen: any remote: any TouchBar: typeof TouchBar + BrowserWindow: typeof BrowserWindow private electron: any constructor () { @@ -27,6 +28,7 @@ export class ElectronService { this.globalShortcut = this.remote.globalShortcut this.nativeImage = this.remote.nativeImage this.TouchBar = this.remote.TouchBar + this.BrowserWindow = this.remote.BrowserWindow } remoteRequire (name: string): any { diff --git a/terminus-core/src/services/hostApp.service.ts b/terminus-core/src/services/hostApp.service.ts index 40d5ca0e..e18e858e 100644 --- a/terminus-core/src/services/hostApp.service.ts +++ b/terminus-core/src/services/hostApp.service.ts @@ -1,8 +1,8 @@ import * as path from 'path' import { Observable, Subject } from 'rxjs' import { Injectable, NgZone, EventEmitter } from '@angular/core' -import { ElectronService } from '../services/electron.service' -import { Logger, LogService } from '../services/log.service' +import { ElectronService } from './electron.service' +import { Logger, LogService } from './log.service' export enum Platform { Linux, macOS, Windows, @@ -19,19 +19,21 @@ export interface Bounds { export class HostAppService { platform: Platform nodePlatform: string - ready = new EventEmitter() shown = new EventEmitter() isFullScreen = false private preferencesMenu = new Subject() private secondInstance = new Subject() private cliOpenDirectory = new Subject() private cliRunCommand = new Subject() + private configChangeBroadcast = new Subject() private logger: Logger + private windowId: number get preferencesMenu$ (): Observable { return this.preferencesMenu } get secondInstance$ (): Observable { return this.secondInstance } get cliOpenDirectory$ (): Observable { return this.cliOpenDirectory } get cliRunCommand$ (): Observable { return this.cliRunCommand } + get configChangeBroadcast$ (): Observable { return this.configChangeBroadcast } constructor ( private zone: NgZone, @@ -46,9 +48,12 @@ export class HostAppService { linux: Platform.Linux }[this.nodePlatform] + this.windowId = parseInt(location.search.substring(1)) + this.logger.info('Window ID:', this.windowId) + electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next())) - electron.ipcRenderer.on('uncaughtException', ($event, err) => { + electron.ipcRenderer.on('uncaughtException', (_$event, err) => { this.logger.error('Unhandled exception:', err) }) @@ -64,7 +69,7 @@ export class HostAppService { this.zone.run(() => this.shown.emit()) }) - electron.ipcRenderer.on('host:second-instance', ($event, argv: any, cwd: string) => this.zone.run(() => { + electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => { this.logger.info('Second instance', argv) const op = argv._[0] if (op === 'open') { @@ -74,13 +79,17 @@ export class HostAppService { } })) - this.ready.subscribe(() => { - electron.ipcRenderer.send('app:ready') - }) + electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => { + this.configChangeBroadcast.next() + })) } getWindow () { - return this.electron.app.window + return this.electron.BrowserWindow.fromId(this.windowId) + } + + newWindow () { + this.electron.ipcRenderer.send('app:new-window') } getShell () { @@ -142,6 +151,14 @@ export class HostAppService { } } + broadcastConfigChange () { + this.electron.ipcRenderer.send('app:config-change') + } + + emitReady () { + this.electron.ipcRenderer.send('app:ready') + } + quit () { this.logger.info('Quitting') this.electron.app.quit() diff --git a/terminus-core/src/services/hotkeys.service.ts b/terminus-core/src/services/hotkeys.service.ts index 43830229..760c0c73 100644 --- a/terminus-core/src/services/hotkeys.service.ts +++ b/terminus-core/src/services/hotkeys.service.ts @@ -174,6 +174,10 @@ export class HotkeysService { @Injectable() export class AppHotkeyProvider extends HotkeyProvider { hotkeys: IHotkeyDescription[] = [ + { + id: 'new-window', + name: 'New window', + }, { id: 'toggle-window', name: 'Toggle terminal window', diff --git a/terminus-core/src/services/touchbar.service.ts b/terminus-core/src/services/touchbar.service.ts index ad9db0e3..ea1b0b09 100644 --- a/terminus-core/src/services/touchbar.service.ts +++ b/terminus-core/src/services/touchbar.service.ts @@ -3,6 +3,7 @@ import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron' import { AppService } from './app.service' import { ConfigService } from './config.service' import { ElectronService } from './electron.service' +import { HostAppService } from './hostApp.service' import { IToolbarButton, ToolbarButtonProvider } from '../api' @Injectable() @@ -12,6 +13,7 @@ export class TouchbarService { constructor ( private app: AppService, + private hostApp: HostAppService, @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[], private config: ConfigService, private electron: ElectronService, @@ -51,7 +53,7 @@ export class TouchbarService { ...buttons.map(button => this.getButton(button)) ] }) - this.electron.app.window.setTouchBar(touchBar) + this.hostApp.getWindow().setTouchBar(touchBar) } private getButton (button: IToolbarButton): Electron.TouchBarButton { diff --git a/terminus-terminal/src/buttonProvider.ts b/terminus-terminal/src/buttonProvider.ts index 6cbe8db8..8de05816 100644 --- a/terminus-terminal/src/buttonProvider.ts +++ b/terminus-terminal/src/buttonProvider.ts @@ -17,7 +17,12 @@ export class ButtonProvider extends ToolbarButtonProvider { super() hotkeys.matchedHotkey.subscribe(async (hotkey) => { if (hotkey === 'new-tab') { - this.terminal.openTab() + terminal.openTab() + } + }) + hotkeys.matchedHotkey.subscribe(async (hotkey) => { + if (hotkey === 'new-window') { + hostApp.newWindow() } }) hostApp.cliOpenDirectory$.subscribe(async directory => { diff --git a/yarn.lock b/yarn.lock index d2aad41d..1a5cdaf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -148,6 +148,10 @@ "@types/mz" "*" "@types/node" "*" +"@types/js-yaml@^3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.11.2.tgz#699ad86054cc20043c30d66a6fcde30bbf5d3d5e" + "@types/mz@*": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.32.tgz#e8248b4e41424c052edc1725dd33650c313a3659" From dc4b984ed061d95ad2f5f0a66b4bd21eaaf1ae63 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 31 Aug 2018 16:57:10 +0200 Subject: [PATCH 4/4] . --- app/src/entry.preload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/entry.preload.ts b/app/src/entry.preload.ts index 2bd8b06d..73901420 100644 --- a/app/src/entry.preload.ts +++ b/app/src/entry.preload.ts @@ -30,7 +30,7 @@ Raven.config( ) process.on('uncaughtException' as any, (err) => { - Raven.captureException(err) + Raven.captureException(err as any) console.error(err) })