From 86cb06e25ed2772ae40b255fff9f9079b3db5ca2 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Wed, 22 Mar 2017 00:18:52 +0100 Subject: [PATCH] . --- app/main.js | 2 +- app/src/app.module.ts | 8 +- app/src/components/app.less | 133 +++------------------------ app/src/components/app.pug | 27 +++--- app/src/components/app.ts | 43 +++------ app/src/components/baseTab.ts | 12 +++ app/src/components/settingsPane.less | 3 + app/src/components/settingsPane.ts | 6 +- app/src/components/tabBody.scss | 18 ++++ app/src/components/tabBody.ts | 29 ++++++ app/src/components/tabHeader.pug | 4 + app/src/components/tabHeader.scss | 80 ++++++++++++++++ app/src/components/tabHeader.ts | 17 ++++ app/src/components/terminal.scss | 2 + app/src/components/terminal.ts | 27 +++--- app/src/models/tab.ts | 64 +++++++++++++ app/src/services/sessions.ts | 87 ++++++++++++------ app/src/theme.scss | 58 ++++++++++++ app/src/variables.scss | 1 + webpack.config.js | 6 +- 20 files changed, 420 insertions(+), 207 deletions(-) create mode 100644 app/src/components/baseTab.ts create mode 100644 app/src/components/tabBody.scss create mode 100644 app/src/components/tabBody.ts create mode 100644 app/src/components/tabHeader.pug create mode 100644 app/src/components/tabHeader.scss create mode 100644 app/src/components/tabHeader.ts create mode 100644 app/src/models/tab.ts create mode 100644 app/src/variables.scss diff --git a/app/main.js b/app/main.js index 9ad1acfb..189080a6 100644 --- a/app/main.js +++ b/app/main.js @@ -138,7 +138,7 @@ start = () => { //- background to avoid the flash of unstyled window backgroundColor: '#1D272D', frame: false, - type: 'toolbar', + //type: 'toolbar', } Object.assign(options, windowConfig.get('windowBoundaries')) diff --git a/app/src/app.module.ts b/app/src/app.module.ts index d1d110b0..c8fc2be5 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -24,6 +24,8 @@ import { HotkeyDisplayComponent } from 'components/hotkeyDisplay' import { HotkeyHintComponent } from 'components/hotkeyHint' import { HotkeyInputModalComponent } from 'components/hotkeyInputModal' import { SettingsPaneComponent } from 'components/settingsPane' +import { TabBodyComponent } from 'components/tabBody' +import { TabHeaderComponent } from 'components/tabHeader' import { TerminalComponent } from 'components/terminal' @@ -50,6 +52,8 @@ import { TerminalComponent } from 'components/terminal' ], entryComponents: [ HotkeyInputModalComponent, + SettingsPaneComponent, + TerminalComponent, ], declarations: [ AppComponent, @@ -59,10 +63,12 @@ import { TerminalComponent } from 'components/terminal' HotkeyInputComponent, HotkeyInputModalComponent, SettingsPaneComponent, + TabBodyComponent, + TabHeaderComponent, TerminalComponent, ], bootstrap: [ - AppComponent + AppComponent, ] }) export class AppModule { diff --git a/app/src/components/app.less b/app/src/components/app.less index 6a15a693..a25165ba 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -29,7 +29,7 @@ background: @body-bg; } -@titlebar-height: 35px; +@titlebar-height: 30px; @tabs-height: 40px; @tab-border-radius: 4px; @@ -51,6 +51,10 @@ box-shadow: none; border-radius: 0; font-size: 8px; + width: 40px; + padding: 0; + line-height: @titlebar-height; + text-align: center; &:not(:hover):not(:active) { background: transparent; @@ -62,6 +66,11 @@ } } +:host > .spacer { + flex: 0 0 5px; + background: @title-bg; +} + .tabs { flex: none; height: @tabs-height; @@ -69,12 +78,10 @@ display: flex; flex-direction: row; - &>button, .tab { + &>button { line-height: @tabs-height - 2px; cursor: pointer; - } - &>button { padding: 0 15px; flex: 0 0 auto; border-bottom: 2px solid transparent; @@ -96,130 +103,14 @@ border-bottom-right-radius: @tab-border-radius; } - .tab.active + button { + tab-header.active + button { border-bottom-left-radius: @tab-border-radius; } - - .tab { - flex: auto; - flex-basis: 0; - flex-grow: 1000; - - display: flex; - overflow: hidden; - - min-width: 0; - background: @body-bg; - transition: 0.25s all; - - .button-states(); - - .content-wrapper { - display: flex; - flex-direction: row; - flex: auto; - min-width: 0; - background: @title-bg; - transition: 0.25s all; - - div.index { - flex: none; - padding: 0 0 0 15px; - font-weight: bold; - color: #444; - } - - div.name { - flex: auto; - margin: 0 1px 0 10px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - min-width: 0; - } - - button { - flex: none; - - background: transparent; - color: @text-color; - - display: block; - opacity: 0; - - @button-size: @tabs-height * 0.6; - width: @button-size; - height: @button-size; - border-radius: @button-size / 2; - line-height: @button-size * 0.8; - margin-top: (@tabs-height - @button-size) * 0.4; - margin-right: 10px; - - text-align: center; - font-size: 20px; - - .button-states(); - } - - &:hover button { - transition: 0.25s opacity; - display: block; - opacity: 1; - } - } - - //border-bottom: 2px solid transparent; - transition: 0.25s all; - - &.pre-selected, &:nth-last-child(1) { - .content-wrapper { - border-bottom-right-radius: @tab-border-radius; - } - } - - &.post-selected { - .content-wrapper { - border-bottom-left-radius: @tab-border-radius; - } - } - - &.active { - background: @title-bg; - box-shadow: 0px -1px 0px 0px blue; - - .content-wrapper { - //border-bottom: 2px solid #69bbea; - background: @body-bg; - border-top-left-radius: @tab-border-radius; - border-top-right-radius: @tab-border-radius; - } - } - } } .tabs-content { flex: auto; display: flex; - - .tab { - display: none; - flex: auto; - position: relative; - padding: 15px; - - overflow: hidden; - &.scrollable { - overflow-y: auto; - } - - &.active { - display: flex; - - >* { - flex: auto; - } - } - } } hotkey-hint { diff --git a/app/src/components/app.pug b/app/src/components/app.pug index e018efdc..c3fd8952 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -7,32 +7,33 @@ button.btn.btn-secondary.btn-close((click)='hostApp.quit()') i.fa.fa-close +.spacer + .tabs(class='active-tab-{{tabs.indexOf(activeTab)}}') button.btn.btn-secondary.btn-new-tab((click)='newTab()') i.fa.fa-plus - .tab( + tab-header( *ngFor='let tab of tabs; let idx = index; trackBy: tab?.id', - (click)='selectTab(tab)', - [class.active]='tab == activeTab', - [class.pre-selected]='tabs[idx + 1] == activeTab', - [class.post-selected]='tabs[idx - 1] == activeTab', + [index]='idx', + [model]='tab', + [active]='tab == activeTab', + [hasActivity]='tab.hasActivity', @animateTab, + (click)='selectTab(tab)', + (closeClicked)='closeTab(tab)', ) - .content-wrapper - div.index {{idx + 1}} - div.name {{tab.name || 'Terminal'}} - button((click)='closeTab(tab)') × button.btn.btn-secondary.btn-settings((click)='showSettings()') i.fa.fa-cog .tabs-content - .tab( + tab-body( *ngFor='let tab of tabs; trackBy: tab?.id', - [class.active]='tab == activeTab', + [active]='tab == activeTab', + [model]='tab', [class.scrollable]='tab.scrollable', ) - terminal(*ngIf='tab.type == "terminal"', [session]='tab.session', '[(title)]'='tab.name') - settings-pane(*ngIf='tab.type == "settings"') + //-terminal(*ngIf='tab.type == "terminal"', [session]='tab.session', '[(title)]'='tab.name') + //-settings-pane(*ngIf='tab.type == "settings"') hotkey-hint diff --git a/app/src/components/app.ts b/app/src/components/app.ts index 21d8f050..694ed697 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, trigger, style, animate, transition, state } from '@angular/core' +import { Component, ElementRef, Input, trigger, style, animate, transition, state } from '@angular/core' import { ToasterConfig } from 'angular2-toaster' import { ElectronService } from 'services/electron' @@ -8,31 +8,15 @@ import { LogService } from 'services/log' import { QuitterService } from 'services/quitter' import { ConfigService } from 'services/config' import { DockingService } from 'services/docking' -import { Session, SessionsService } from 'services/sessions' +import { SessionsService } from 'services/sessions' + +import { Tab, SettingsTab, TerminalTab } from 'models/tab' import 'angular2-toaster/lib/toaster.css' import 'global.less' import 'theme.scss' -const TYPE_TERMINAL = 'terminal' -const TYPE_SETTINGS = 'settings' - -class Tab { - id: number - name: string - scrollable: boolean - static lastTabID = 0 - - constructor (public type: string, public session: Session) { - this.id = Tab.lastTabID++ - if (type == TYPE_SETTINGS) { - this.name = 'Settings' - } - } -} - - @Component({ selector: 'app', template: require('./app.pug'), @@ -58,8 +42,8 @@ class Tab { }) export class AppComponent { toasterConfig: ToasterConfig - tabs: Tab[] = [] - activeTab: Tab + @Input() tabs: Tab[] = [] + @Input() activeTab: Tab lastTabIndex = 0 constructor( @@ -161,11 +145,11 @@ export class AppComponent { } newTab () { - this.addTerminalTab(this.sessions.createNewSession({shell: 'zsh'})) + this.addTerminalTab(this.sessions.createNewSession({command: 'zsh'})) } addTerminalTab (session) { - let tab = new Tab(TYPE_TERMINAL, session) + let tab = new TerminalTab(session) this.tabs.push(tab) this.selectTab(tab) } @@ -176,6 +160,9 @@ export class AppComponent { } else { this.lastTabIndex = null } + if (this.activeTab) { + this.activeTab.hasActivity = false + } this.activeTab = tab setImmediate(() => { let iframe = this.elementRef.nativeElement.querySelector(':scope .tab.active iframe') @@ -207,8 +194,9 @@ export class AppComponent { } closeTab (tab) { + tab.destroy() if (tab.session) { - tab.session.gracefullyDestroy() + this.sessions.destroySession(tab.session) } let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1) this.tabs = this.tabs.filter((x) => x != tab) @@ -231,10 +219,9 @@ export class AppComponent { } showSettings() { - let settingsTab = this.tabs.find((x) => x.type == TYPE_SETTINGS) + let settingsTab = this.tabs.find((x) => x instanceof SettingsTab) if (!settingsTab) { - settingsTab = new Tab(TYPE_SETTINGS, null) - settingsTab.scrollable = true + settingsTab = new SettingsTab() this.tabs.push(settingsTab) } this.selectTab(settingsTab) diff --git a/app/src/components/baseTab.ts b/app/src/components/baseTab.ts new file mode 100644 index 00000000..fae14097 --- /dev/null +++ b/app/src/components/baseTab.ts @@ -0,0 +1,12 @@ +import { Tab } from 'models/tab' + +export class BaseTabComponent { + protected model: T + + initModel (model: T) { + this.model = model + this.initTab() + } + + initTab () { } +} diff --git a/app/src/components/settingsPane.less b/app/src/components/settingsPane.less index f821fb80..7f4abb6a 100644 --- a/app/src/components/settingsPane.less +++ b/app/src/components/settingsPane.less @@ -1,4 +1,7 @@ :host { + flex: auto; + margin: 15px; + >.btn-block { margin-bottom: 20px; } diff --git a/app/src/components/settingsPane.ts b/app/src/components/settingsPane.ts index 2bdd76d0..2c1f319d 100644 --- a/app/src/components/settingsPane.ts +++ b/app/src/components/settingsPane.ts @@ -9,13 +9,16 @@ import 'rxjs/add/operator/debounceTime' import 'rxjs/add/operator/distinctUntilChanged' const childProcessPromise = nodeRequire('child-process-promise') +import { BaseTabComponent } from 'components/baseTab' +import { SettingsTab } from 'models/tab' + @Component({ selector: 'settings-pane', template: require('./settingsPane.pug'), styles: [require('./settingsPane.less')], }) -export class SettingsPaneComponent { +export class SettingsPaneComponent extends BaseTabComponent { isWindows: boolean isMac: boolean isLinux: boolean @@ -31,6 +34,7 @@ export class SettingsPaneComponent { public docking: DockingService, hostApp: HostAppService, ) { + super() this.isWindows = hostApp.platform == PLATFORM_WINDOWS this.isMac = hostApp.platform == PLATFORM_MAC this.isLinux = hostApp.platform == PLATFORM_LINUX diff --git a/app/src/components/tabBody.scss b/app/src/components/tabBody.scss new file mode 100644 index 00000000..84ea44ff --- /dev/null +++ b/app/src/components/tabBody.scss @@ -0,0 +1,18 @@ +:host { + display: none; + flex: auto; + position: relative; + overflow: hidden; + + &.scrollable { + overflow-y: auto; + } + + &.active { + display: flex; + + >* { + flex: auto; + } + } +} diff --git a/app/src/components/tabBody.ts b/app/src/components/tabBody.ts new file mode 100644 index 00000000..5db3e42d --- /dev/null +++ b/app/src/components/tabBody.ts @@ -0,0 +1,29 @@ +import { Component, Input, ViewContainerRef, ViewChild, HostBinding, ComponentFactoryResolver, ComponentRef } from '@angular/core' +import { Tab } from 'models/tab' +import { BaseTabComponent } from 'components/baseTab' + +@Component({ + selector: 'tab-body', + template: '', + styles: [require('./tabBody.scss')], +}) +export class TabBodyComponent { + @Input() @HostBinding('class.active') active: boolean + @Input() model: Tab + @ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef + private component: ComponentRef> + + constructor (private componentFactoryResolver: ComponentFactoryResolver) { + } + + ngAfterViewInit () { + // run after the change detection finishes + setImmediate(() => { + let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.model.getComponentType()) + this.component = this.placeholder.createComponent(componentFactory) + setImmediate(() => { + this.component.instance.initModel(this.model) + }) + }) + } +} diff --git a/app/src/components/tabHeader.pug b/app/src/components/tabHeader.pug new file mode 100644 index 00000000..cabab87f --- /dev/null +++ b/app/src/components/tabHeader.pug @@ -0,0 +1,4 @@ +.content-wrapper + .index {{index + 1}} + .name {{model.title || "Terminal"}} + button((click)='closeClicked.emit()') × diff --git a/app/src/components/tabHeader.scss b/app/src/components/tabHeader.scss new file mode 100644 index 00000000..79250cbd --- /dev/null +++ b/app/src/components/tabHeader.scss @@ -0,0 +1,80 @@ +@import '~variables.scss'; + +:host { + line-height: $tabs-height - 2px; + cursor: pointer; + + flex: auto; + flex-basis: 0; + flex-grow: 1000; + + display: flex; + overflow: hidden; + + min-width: 0; + transition: 0.25s all; + //.button-states(); + + .content-wrapper { + display: flex; + flex-direction: row; + flex: auto; + min-width: 0; + transition: 0.25s all; + border-top: 1px solid transparent; + + .index { + flex: none; + font-weight: bold; + align-self: center; + + margin-left: 10px; + width: 20px; + height: 20px; + border-radius: 10px; + line-height: 20px; + text-align: center; + transition: 0.25s all; + } + + .name { + flex: auto; + margin: 0 1px 0 10px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; + } + + button { + flex: none; + + background: transparent; + + display: block; + opacity: 0; + + $button-size: $tabs-height * 0.6; + width: $button-size; + height: $button-size; + border-radius: $button-size / 2; + line-height: $button-size * 0.8; + margin-top: ($tabs-height - $button-size) * 0.4; + margin-right: 10px; + + text-align: center; + font-size: 20px; + + //.button-states(); + } + + &:hover button { + transition: 0.25s opacity; + display: block; + opacity: 1; + } + } + + //border-bottom: 2px solid transparent; + transition: 0.25s all; +} diff --git a/app/src/components/tabHeader.ts b/app/src/components/tabHeader.ts new file mode 100644 index 00000000..160edd70 --- /dev/null +++ b/app/src/components/tabHeader.ts @@ -0,0 +1,17 @@ +import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core' +import { Tab } from 'models/tab' + +import './tabHeader.scss' + +@Component({ + selector: 'tab-header', + template: require('./tabHeader.pug'), + styles: [require('./tabHeader.scss')], +}) +export class TabHeaderComponent { + @Input() index: number + @Input() @HostBinding('class.active') active: boolean + @Input() @HostBinding('class.has-activity') hasActivity: boolean + @Input() model: Tab + @Output() closeClicked = new EventEmitter() +} diff --git a/app/src/components/terminal.scss b/app/src/components/terminal.scss index b291db41..95037e8c 100644 --- a/app/src/components/terminal.scss +++ b/app/src/components/terminal.scss @@ -1,7 +1,9 @@ :host { + flex: auto; position: relative; display: block; overflow: hidden; + margin: 15px; div[style]:last-child { background: black !important; diff --git a/app/src/components/terminal.ts b/app/src/components/terminal.ts index 58e69d31..0c64ab7d 100644 --- a/app/src/components/terminal.ts +++ b/app/src/components/terminal.ts @@ -1,9 +1,12 @@ import { Subscription } from 'rxjs' -import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core' +import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core' import { ConfigService } from 'services/config' import { PluginDispatcherService } from 'services/pluginDispatcher' -import { Session } from 'services/sessions' + +import { BaseTabComponent } from 'components/baseTab' +import { TerminalTab } from 'models/tab' + const hterm = require('hterm-commonjs') const dataurl = require('dataurl') @@ -47,8 +50,7 @@ hterm.hterm.Terminal.prototype.showOverlay = () => null template: '', styles: [require('./terminal.scss')], }) -export class TerminalComponent { - @Input() session: Session +export class TerminalComponent extends BaseTabComponent { title: string @Output() titleChange = new EventEmitter() terminal: any @@ -60,12 +62,13 @@ export class TerminalComponent { public config: ConfigService, private pluginDispatcher: PluginDispatcherService, ) { + super() this.configSubscription = config.change.subscribe(() => { this.configure() }) } - ngOnInit () { + initTab () { let io this.terminal = new hterm.hterm.Terminal() this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal }) @@ -78,23 +81,23 @@ export class TerminalComponent { this.terminal.onTerminalReady = () => { this.terminal.installKeyboard() io = this.terminal.io.push() - const dataSubscription = this.session.dataAvailable.subscribe((data) => { - io.writeUTF16(data) + const dataSubscription = this.model.session.dataAvailable.subscribe((data) => { + io.writeUTF8(data) }) - const closedSubscription = this.session.closed.subscribe(() => { + const closedSubscription = this.model.session.closed.subscribe(() => { dataSubscription.unsubscribe() closedSubscription.unsubscribe() }) io.onVTKeystroke = io.sendString = (str) => { - this.session.write(str) + this.model.session.write(str) } io.onTerminalResize = (columns, rows) => { console.log(`Resizing to ${columns}x${rows}`) - this.session.resize(columns, rows) + this.model.session.resize(columns, rows) } - this.session.releaseInitialDataBuffer() + this.model.session.releaseInitialDataBuffer() } this.terminal.decorate(this.elementRef.nativeElement) this.configure() @@ -108,6 +111,8 @@ export class TerminalComponent { preferenceManager.set('audible-bell-sound', '') preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification') preferenceManager.set('enable-clipboard-notice', false) + preferenceManager.set('receive-encoding', 'raw') + preferenceManager.set('send-encoding', 'raw') } ngOnDestroy () { diff --git a/app/src/models/tab.ts b/app/src/models/tab.ts new file mode 100644 index 00000000..538061b3 --- /dev/null +++ b/app/src/models/tab.ts @@ -0,0 +1,64 @@ +import { Subscription } from 'rxjs' +import { Session } from 'services/sessions' + + +export class Tab { + id: number + title: string + scrollable: boolean + hasActivity = false + static lastTabID = 0 + + constructor () { + this.id = Tab.lastTabID++ + } + + getComponentType (): (new (...args: any[])) { + return null + } + + destroy (): void { } +} + + +import { SettingsPaneComponent } from 'components/settingsPane' + +export class SettingsTab extends Tab { + constructor () { + super() + this.title = 'Settings' + this.scrollable = true + } + + getComponentType (): (new (...args: any[])) { + return SettingsPaneComponent + } +} + + +import { TerminalComponent } from 'components/terminal' + +export class TerminalTab extends Tab { + private activitySubscription: Subscription + + constructor (public session: Session) { + super() + // ignore the initial refresh + setTimeout(() => { + this.activitySubscription = this.session.dataAvailable.subscribe(() => { + this.hasActivity = true + }) + }, 500) + } + + getComponentType (): (new (...args: any[])) { + return TerminalComponent + } + + destroy () { + super.destroy() + if (this.activitySubscription) { + this.activitySubscription.unsubscribe() + } + } +} diff --git a/app/src/services/sessions.ts b/app/src/services/sessions.ts index 6023ab00..e4b25bd4 100644 --- a/app/src/services/sessions.ts +++ b/app/src/services/sessions.ts @@ -1,34 +1,38 @@ 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 nodePTY from 'node-pty' import * as fs from 'fs' -export interface SessionRecoveryProvider { - list(): Promise - getRecoveryCommand(item: any): string - getNewSessionCommand(command: string): string +export interface ISessionRecoveryProvider { + list (): Promise + getRecoverySession (recoveryId: any): SessionOptions + wrapNewSession (options: SessionOptions): SessionOptions + terminateSession (recoveryId: string): Promise } -export class NullSessionRecoveryProvider implements SessionRecoveryProvider { - list(): Promise { - return Promise.resolve([]) +export class NullSessionRecoveryProvider implements ISessionRecoveryProvider { + async list (): Promise { + return [] } - getRecoveryCommand(_: any): string { + getRecoverySession (_recoveryId: any): SessionOptions { return null } - getNewSessionCommand(command: string) { - return command + wrapNewSession (options: SessionOptions): SessionOptions { + return options + } + + async terminateSession (_recoveryId: string): Promise { + return null } } -export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider { +export class ScreenSessionRecoveryProvider implements ISessionRecoveryProvider { list(): Promise { - return exec('screen -ls').then((result) => { + return exec('screen -list').then((result) => { return result.stdout.split('\n') .filter((line) => /\bterm-tab-/.exec(line)) .map((line) => line.trim().split('.')[0]) @@ -37,12 +41,14 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider { }) } - getRecoveryCommand(item: any): string { - return `screen -r ${item}` + getRecoverySession (recoveryId: any): SessionOptions { + return { + command: 'screen', + args: ['-r', recoveryId], + } } - getNewSessionCommand(command: string): string { - const id = crypto.randomBytes(8).toString('hex') + wrapNewSession (options: SessionOptions): SessionOptions { // TODO let configPath = '/tmp/.termScreenConfig' fs.writeFileSync(configPath, ` @@ -51,8 +57,19 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider { term xterm-color bindkey "^[OH" beginning-of-line bindkey "^[OF" end-of-line + termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007' + defhstatus "^Et" + hardstatus off `, 'utf-8') - return `screen -c ${configPath} -U -S term-tab-${id} -- ${command}` + let recoveryId = `term-tab-${Date.now()}` + options.args = ['-c', configPath, '-U', '-S', recoveryId, '--', options.command].concat(options.args || []) + options.command = 'screen' + options.recoveryId = recoveryId + return options + } + + async terminateSession (recoveryId: string): Promise { + return exec(`screen -S ${recoveryId} -X quit`) } } @@ -60,9 +77,10 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider { export interface SessionOptions { name?: string, command?: string, - shell?: string, + args?: string[], cwd?: string, env?: any, + recoveryId?: string } export class Session { @@ -71,6 +89,7 @@ export class Session { dataAvailable = new EventEmitter() closed = new EventEmitter() destroyed = new EventEmitter() + recoveryId: string private pty: any private initialDataBuffer = '' private initialDataBufferReleased = false @@ -79,14 +98,16 @@ export class Session { this.name = options.name console.log('Spawning', options.command) - 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, { + if (options.command.includes(' ')) { + options.args = ['-c', options.command] + options.command = 'sh' + } + this.pty = nodePTY.spawn(options.command, options.args || [], { //name: 'screen-256color', name: 'xterm-256color', //name: 'xterm-color', @@ -168,7 +189,7 @@ export class SessionsService { sessions: {[id: string]: Session} = {} logger: Logger private lastID = 0 - recoveryProvider: SessionRecoveryProvider + recoveryProvider: ISessionRecoveryProvider constructor( private zone: NgZone, @@ -180,8 +201,10 @@ export class SessionsService { } createNewSession (options: SessionOptions) : Session { - options.command = this.recoveryProvider.getNewSessionCommand(options.command) - return this.createSession(options) + options = this.recoveryProvider.wrapNewSession(options) + let session = this.createSession(options) + session.recoveryId = options.recoveryId + return session } createSession (options: SessionOptions) : Session { @@ -196,12 +219,20 @@ export class SessionsService { return session } + async destroySession (session: Session): Promise { + await session.gracefullyDestroy() + await this.recoveryProvider.terminateSession(session.recoveryId) + return null + } + recoverAll () : Promise { return >(this.recoveryProvider.list().then((items) => { return this.zone.run(() => { - return items.map((item) => { - const command = this.recoveryProvider.getRecoveryCommand(item) - return this.createSession({command}) + return items.map((recoveryId) => { + const options = this.recoveryProvider.getRecoverySession(recoveryId) + let session = this.createSession(options) + session.recoveryId = recoveryId + return session }) }) })) diff --git a/app/src/theme.scss b/app/src/theme.scss index 114e987e..6cd94beb 100644 --- a/app/src/theme.scss +++ b/app/src/theme.scss @@ -63,3 +63,61 @@ ngb-tabset .tab-content { [ngbradiogroup] > label.active { background: $blue; } + +$tab-border-radius: 5px; + +.tabs tab-header { + background: $body-bg; + .content-wrapper { + background: $body-bg2; + + .index { + color: #444; + } + + button { + color: $body-color; + border: none; + transition: 0.25s all; + + &:hover { + background: rgba(0, 0, 0, .25) !important; + } + + &:active { + background: rgba(0, 0, 0, .5) !important; + } + } + } + + &.pre-selected, &:nth-last-child(1) { + .content-wrapper { + border-bottom-right-radius: $tab-border-radius; + } + } + + &.post-selected { + .content-wrapper { + border-bottom-left-radius: $tab-border-radius; + } + } + + &.active { + background: $body-bg2; + + .content-wrapper { + border-top: 1px solid $blue; + background: $body-bg; + border-top-left-radius: $tab-border-radius; + border-top-right-radius: $tab-border-radius; + } + } + + &.has-activity:not(.active) { + .content-wrapper .index { + background: $blue; + color: white; + text-shadow: 0 1px 1px rgba(0,0,0,.95); + } + } +} diff --git a/app/src/variables.scss b/app/src/variables.scss new file mode 100644 index 00000000..fb8115fe --- /dev/null +++ b/app/src/variables.scss @@ -0,0 +1 @@ +$tabs-height: 40px; diff --git a/webpack.config.js b/webpack.config.js index aff189a0..45aa7017 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,7 +23,7 @@ module.exports = { loaders: [ { test: /\.ts$/, - loader: 'awesome-typescript-loader' + loader: 'awesome-typescript-loader', }, { test: /\.pug$/, @@ -63,14 +63,14 @@ module.exports = { { test: /\.(png|svg)$/, loader: "file-loader", - query: { + options: { name: 'images/[name].[hash:8].[ext]' } }, { test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader", - query: { + options: { name: 'fonts/[name].[hash:8].[ext]' } },