diff --git a/app/main.js b/app/main.js index 394a0ee1..5f09dd7b 100644 --- a/app/main.js +++ b/app/main.js @@ -27,10 +27,21 @@ setupWindowManagement = () => { }) electron.ipcMain.on('window-focus', () => { - app.window.show() app.window.focus() }) + electron.ipcMain.on('window-focus', () => { + app.window.focus() + }) + + electron.ipcMain.on('window-toggle-focus', () => { + if (app.window.isFocused()) { + app.window.minimize() + } else { + app.window.focus() + } + }) + electron.ipcMain.on('window-maximize', () => { if (app.window.isMaximized()) { app.window.unmaximize() diff --git a/app/src/components/app.less b/app/src/components/app.less index ca38a5db..28f7a665 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -92,7 +92,7 @@ .tab { flex: auto; flex-basis: 0; - flex-grow: 1; + flex-grow: 1000; background: @body-bg; @@ -178,6 +178,7 @@ display: none; flex: auto; position: relative; + padding: 10px 15px; &.active { display: flex; diff --git a/app/src/components/app.pug b/app/src/components/app.pug index a7b4bfc3..c05e3710 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -14,6 +14,7 @@ [class.active]='tab == activeTab', [class.pre-selected]='tabs[idx + 1] == activeTab', [class.post-selected]='tabs[idx - 1] == activeTab', + @animateTab, ) div.index {{idx + 1}} div.name {{tab.name || 'Terminal'}} diff --git a/app/src/components/app.ts b/app/src/components/app.ts index af8d5628..dd1b4290 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef } from '@angular/core' +import { Component, ElementRef, trigger, style, animate, transition, state } from '@angular/core' import { ModalService } from 'services/modal' import { ElectronService } from 'services/electron' import { HostAppService } from 'services/hostApp' @@ -29,6 +29,24 @@ class Tab { selector: 'app', template: require('./app.pug'), styles: [require('./app.less')], + animations: [ + trigger('animateTab', [ + state('in', style({ + 'flex-grow': '1000', + })), + transition(':enter', [ + style({ + 'flex-grow': '1', + }), + animate('250ms ease-in-out') + ]), + transition(':leave', [ + animate('250ms ease-in-out', style({ + 'flex-grow': '1', + })) + ]) + ]) + ] }) export class AppComponent { constructor( @@ -65,8 +83,19 @@ export class AppComponent { this.selectTab(this.tabs[9]) } } + if (key.ctrl && key.shift && key.key == 'W' && this.activeTab) { + this.closeTab(this.activeTab) + } + if (key.ctrl && key.shift && key.key == 'T' && this.activeTab) { + this.newTab() + } } }) + + this.hotkeys.registerHotkeys() + this.hotkeys.globalHotkey.subscribe(() => { + this.hostApp.toggleWindow() + }) } toasterConfig: ToasterConfig @@ -92,9 +121,10 @@ export class AppComponent { closeTab (tab) { tab.session.gracefullyDestroy() + let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1) this.tabs = this.tabs.filter((x) => x != tab) if (tab == this.activeTab) { - this.selectTab(this.tabs[0]) + this.selectTab(this.tabs[newIndex]) } } diff --git a/app/src/components/terminal.ts b/app/src/components/terminal.ts index 5fb1660e..097ce1bb 100644 --- a/app/src/components/terminal.ts +++ b/app/src/components/terminal.ts @@ -20,7 +20,12 @@ hterm.hterm.VT.ESC['k'] = function(parseState) { } hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() -hterm.hterm.PreferenceManager.defaultPreferences['user-css'] = `` +const pmgr = new hterm.hterm.PreferenceManager('default') +pmgr.set('user-css', ``) +pmgr.set('background-color', '#1D272D') +pmgr.set('color-palette-overrides', { + 0: '#1D272D', +}) const oldDecorate = hterm.hterm.ScrollPort.prototype.decorate hterm.hterm.ScrollPort.prototype.decorate = function (...args) { oldDecorate.bind(this)(...args) @@ -73,6 +78,8 @@ export class TerminalComponent { console.log(`Resizing to ${columns}x${rows}`) this.session.resize(columns, rows) } + + this.session.releaseInitialDataBuffer() } this.terminal.decorate(this.elementRef.nativeElement) } diff --git a/app/src/services/electron.ts b/app/src/services/electron.ts index 35e29cfa..e8e9775a 100644 --- a/app/src/services/electron.ts +++ b/app/src/services/electron.ts @@ -19,6 +19,7 @@ export class ElectronService { this.shell = this.electron.shell this.clipboard = this.electron.clipboard this.ipcRenderer = this.electron.ipcRenderer + this.globalShortcut = this.remoteElectron.globalShortcut } initTest() { @@ -34,6 +35,7 @@ export class ElectronService { shell: any dialog: any clipboard: any + globalShortcut: any private electron: any private remoteElectron: any } diff --git a/app/src/services/hostApp.ts b/app/src/services/hostApp.ts index 32317825..bbb80cd4 100644 --- a/app/src/services/hostApp.ts +++ b/app/src/services/hostApp.ts @@ -62,6 +62,10 @@ export class HostAppService { this.electron.ipcRenderer.send('window-focus') } + toggleWindow() { + this.electron.ipcRenderer.send('window-toggle-focus') + } + minimizeWindow () { this.electron.ipcRenderer.send('window-minimize') } diff --git a/app/src/services/hotkeys.ts b/app/src/services/hotkeys.ts index d4d52092..3c1ebbc9 100644 --- a/app/src/services/hotkeys.ts +++ b/app/src/services/hotkeys.ts @@ -1,4 +1,5 @@ import { Injectable, NgZone, EventEmitter } from '@angular/core' +import { ElectronService } from 'services/electron' const hterm = require('hterm-commonjs') @@ -14,8 +15,12 @@ export interface Key { @Injectable() export class HotkeysService { key = new EventEmitter() + globalHotkey = new EventEmitter() - constructor(private zone: NgZone) { + constructor( + private zone: NgZone, + private electron: ElectronService, + ) { let events = [ { name: 'keydown', @@ -45,7 +50,6 @@ export class HotkeysService { } emitNativeEvent (name, nativeEvent) { - console.debug('Key', nativeEvent) this.zone.run(() => { this.key.emit({ event: name, @@ -57,4 +61,11 @@ export class HotkeysService { }) }) } + + registerHotkeys () { + this.electron.globalShortcut.unregisterAll() + this.electron.globalShortcut.register('`', () => { + this.globalHotkey.emit() + }) + } } diff --git a/app/src/services/sessions.ts b/app/src/services/sessions.ts index e05c5aa6..0a508538 100644 --- a/app/src/services/sessions.ts +++ b/app/src/services/sessions.ts @@ -57,17 +57,20 @@ export interface SessionOptions { export class Session { open: boolean name: string - pty: any dataAvailable = new EventEmitter() closed = new EventEmitter() destroyed = new EventEmitter() + private pty: any + private initialDataBuffer = '' + private initialDataBufferReleased = false 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', //name: 'xterm-color', - name: 'xterm-256color', cols: 80, rows: 30, cwd: options.cwd || process.env.HOME, @@ -77,7 +80,11 @@ export class Session { this.open = true this.pty.on('data', (data) => { - this.dataAvailable.emit(data) + if (!this.initialDataBufferReleased) { + this.initialDataBuffer += data + } else { + this.dataAvailable.emit(data) + } }) this.pty.on('close', () => { @@ -86,6 +93,12 @@ export class Session { }) } + releaseInitialDataBuffer () { + this.initialDataBufferReleased = true + this.dataAvailable.emit(this.initialDataBuffer) + this.initialDataBuffer = null + } + resize (columns, rows) { this.pty.resize(columns, rows) } @@ -143,8 +156,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 {