1
1
mirror of https://github.com/Eugeny/tabby.git synced 2025-01-03 16:12:50 +03:00

proper tab classes

This commit is contained in:
Eugene Pankov 2017-04-08 14:50:10 +02:00
parent 2cca57e0fb
commit 79cd2a3bbb
27 changed files with 208 additions and 227 deletions

View File

@ -1,3 +1,3 @@
export abstract class DefaultTabProvider { export abstract class DefaultTabProvider {
abstract open (): void abstract async openNewTab (): Promise<void>
} }

View File

@ -1,4 +1,4 @@
export { Tab } from './tab' export { BaseTabComponent } from '../components/baseTab'
export { TabRecoveryProvider } from './tabRecovery' export { TabRecoveryProvider } from './tabRecovery'
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider' export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider' export { ConfigProvider } from './configProvider'

View File

@ -1,32 +0,0 @@
import { EventEmitter } from '@angular/core'
import { BaseTabComponent } from 'components/baseTab'
export declare type ComponentType<T extends Tab> = new (...args: any[]) => BaseTabComponent<T>
export abstract class Tab {
id: number
title: string
scrollable: boolean
hasActivity = false
focused = new EventEmitter<any>()
blurred = new EventEmitter<any>()
static lastTabID = 0
constructor () {
this.id = Tab.lastTabID++
}
displayActivity (): void {
this.hasActivity = true
}
abstract getComponentType (): ComponentType<Tab>
getRecoveryToken (): any {
return null
}
destroy (): void {
}
}

View File

@ -1,5 +1,3 @@
import { Tab } from './tab'
export abstract class TabRecoveryProvider { export abstract class TabRecoveryProvider {
abstract async recover (recoveryToken: any): Promise<Tab> abstract async recover (recoveryToken: any): Promise<void>
} }

View File

@ -17,6 +17,7 @@ import { NotifyService } from 'services/notify'
import { PluginsService } from 'services/plugins' import { PluginsService } from 'services/plugins'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
import { TabRecoveryService } from 'services/tabRecovery'
import { AppRootComponent } from 'components/appRoot' import { AppRootComponent } from 'components/appRoot'
import { CheckboxComponent } from 'components/checkbox' import { CheckboxComponent } from 'components/checkbox'
@ -53,6 +54,7 @@ let plugins = [
ModalService, ModalService,
NotifyService, NotifyService,
PluginsService, PluginsService,
TabRecoveryService,
QuitterService, QuitterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true }, { provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
], ],

View File

@ -17,7 +17,7 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear
[class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1', [class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1',
[class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1', [class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1',
[index]='idx', [index]='idx',
[model]='tab', [tab]='tab',
[active]='tab == app.activeTab', [active]='tab == app.activeTab',
[hasActivity]='tab.hasActivity', [hasActivity]='tab.hasActivity',
@animateTab, @animateTab,
@ -36,7 +36,7 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear
tab-body( tab-body(
*ngFor='let tab of app.tabs; trackBy: tab?.id', *ngFor='let tab of app.tabs; trackBy: tab?.id',
[active]='tab == app.activeTab', [active]='tab == app.activeTab',
[model]='tab', [tab]='tab',
[class.scrollable]='tab.scrollable', [class.scrollable]='tab.scrollable',
) )

View File

@ -5,10 +5,11 @@ import { ToasterConfig } from 'angular2-toaster'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp' import { HostAppService } from 'services/hostApp'
import { HotkeysService } from 'services/hotkeys' import { HotkeysService } from 'services/hotkeys'
import { LogService } from 'services/log' import { Logger, LogService } from 'services/log'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
import { TabRecoveryService } from 'services/tabRecovery'
import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api' import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api'
@ -43,10 +44,12 @@ import 'theme.scss'
}) })
export class AppRootComponent { export class AppRootComponent {
toasterConfig: ToasterConfig toasterConfig: ToasterConfig
logger: Logger
constructor( constructor(
private docking: DockingService, private docking: DockingService,
private electron: ElectronService, private electron: ElectronService,
private tabRecovery: TabRecoveryService,
public hostApp: HostAppService, public hostApp: HostAppService,
public hotkeys: HotkeysService, public hotkeys: HotkeysService,
public config: ConfigService, public config: ConfigService,
@ -57,8 +60,8 @@ export class AppRootComponent {
) { ) {
console.timeStamp('AppComponent ctor') console.timeStamp('AppComponent ctor')
let logger = log.create('main') this.logger = log.create('main')
logger.info('v', electron.app.getVersion()) this.logger.info('v', electron.app.getVersion())
this.toasterConfig = new ToasterConfig({ this.toasterConfig = new ToasterConfig({
mouseoverTimerStop: true, mouseoverTimerStop: true,
@ -131,7 +134,9 @@ export class AppRootComponent {
getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true) } getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true) }
async ngOnInit () { async ngOnInit () {
await this.app.restoreTabs() await this.tabRecovery.recoverTabs()
this.tabRecovery.saveTabs(this.app.tabs)
if (this.app.tabs.length == 0) { if (this.app.tabs.length == 0) {
this.app.openDefaultTab() this.app.openDefaultTab()
} }

View File

@ -1,12 +1,29 @@
import { Tab } from 'api/tab' import { BehaviorSubject } from 'rxjs'
import { EventEmitter, ViewRef } from '@angular/core'
export class BaseTabComponent<T extends Tab> {
protected model: T
initModel (model: T) { export abstract class BaseTabComponent {
this.model = model id: number
this.initTab() title$ = new BehaviorSubject<string>(null)
scrollable: boolean
hasActivity = false
focused = new EventEmitter<any>()
blurred = new EventEmitter<any>()
hostView: ViewRef
private static lastTabID = 0
constructor () {
this.id = BaseTabComponent.lastTabID++
} }
initTab () { } displayActivity (): void {
this.hasActivity = true
}
getRecoveryToken (): any {
return null
}
destroy (): void {
}
} }

View File

@ -1,5 +1,4 @@
import { Component, Input, ViewContainerRef, ViewChild, HostBinding, ComponentFactoryResolver, ComponentRef } from '@angular/core' import { Component, Input, ViewChild, HostBinding, ViewContainerRef } from '@angular/core'
import { Tab } from 'api/tab'
import { BaseTabComponent } from 'components/baseTab' import { BaseTabComponent } from 'components/baseTab'
@Component({ @Component({
@ -9,21 +8,12 @@ import { BaseTabComponent } from 'components/baseTab'
}) })
export class TabBodyComponent { export class TabBodyComponent {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() model: Tab @Input() tab: BaseTabComponent
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
private component: ComponentRef<BaseTabComponent<Tab>>
constructor (private componentFactoryResolver: ComponentFactoryResolver) {
}
ngAfterViewInit () { ngAfterViewInit () {
// run after the change detection finishes
setImmediate(() => { setImmediate(() => {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.model.getComponentType()) this.placeholder.insert(this.tab.hostView)
this.component = this.placeholder.createComponent(componentFactory)
setImmediate(() => {
this.component.instance.initModel(this.model)
})
}) })
} }
} }

View File

@ -1,4 +1,4 @@
.content-wrapper .content-wrapper
.index {{index + 1}} .index {{index + 1}}
.name {{model.title || "Terminal"}} .name {{(tab.title$ || "Terminal") | async}}
button((click)='closeClicked.emit()') &times; button((click)='closeClicked.emit()') &times;

View File

@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core' import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'
import { Tab } from 'api/tab' import { BaseTabComponent } from 'components/baseTab'
import './tabHeader.scss' import './tabHeader.scss'
@ -12,6 +12,6 @@ export class TabHeaderComponent {
@Input() index: number @Input() index: number
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() @HostBinding('class.has-activity') hasActivity: boolean @Input() @HostBinding('class.has-activity') hasActivity: boolean
@Input() model: Tab @Input() tab: BaseTabComponent
@Output() closeClicked = new EventEmitter() @Output() closeClicked = new EventEmitter()
} }

View File

@ -1,36 +1,49 @@
import { Inject, Injectable } from '@angular/core' import { Subject } from 'rxjs'
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
import { Logger, LogService } from 'services/log' import { Logger, LogService } from 'services/log'
import { Tab } from 'api/tab'
import { TabRecoveryProvider } from 'api/tabRecovery'
import { DefaultTabProvider } from 'api/defaultTabProvider' import { DefaultTabProvider } from 'api/defaultTabProvider'
import { BaseTabComponent } from 'components/baseTab'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@Injectable() @Injectable()
export class AppService { export class AppService {
tabs: Tab[] = [] tabs: BaseTabComponent[] = []
activeTab: Tab activeTab: BaseTabComponent
lastTabIndex = 0 lastTabIndex = 0
logger: Logger logger: Logger
tabsChanged$ = new Subject()
constructor ( constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], private componentFactoryResolver: ComponentFactoryResolver,
private defaultTabProvider: DefaultTabProvider, @Optional() private defaultTabProvider: DefaultTabProvider,
private injector: Injector,
log: LogService, log: LogService,
) { ) {
this.logger = log.create('app') this.logger = log.create('app')
} }
openTab (tab: Tab): void { openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
this.tabs.push(tab) let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
this.selectTab(tab) let componentRef = componentFactory.create(this.injector)
this.saveTabs() componentRef.instance.hostView = componentRef.hostView
Object.assign(componentRef.instance, inputs || {})
this.tabs.push(componentRef.instance)
this.selectTab(componentRef.instance)
this.tabsChanged$.next()
return componentRef.instance
} }
openDefaultTab (): void { openDefaultTab (): void {
if (this.defaultTabProvider) {
this.defaultTabProvider.openNewTab()
}
} }
selectTab (tab) { selectTab (tab: BaseTabComponent) {
if (this.tabs.includes(this.activeTab)) { if (this.tabs.includes(this.activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this.activeTab) this.lastTabIndex = this.tabs.indexOf(this.activeTab)
} else { } else {
@ -41,7 +54,9 @@ export class AppService {
this.activeTab.blurred.emit() this.activeTab.blurred.emit()
} }
this.activeTab = tab this.activeTab = tab
this.activeTab.focused.emit() if (this.activeTab) {
this.activeTab.focused.emit()
}
} }
toggleLastTab () { toggleLastTab () {
@ -65,7 +80,7 @@ export class AppService {
} }
} }
closeTab (tab) { closeTab (tab: BaseTabComponent) {
tab.destroy() tab.destroy()
/* if (tab.session) { /* if (tab.session) {
this.sessions.destroySession(tab.session) this.sessions.destroySession(tab.session)
@ -75,38 +90,6 @@ export class AppService {
if (tab == this.activeTab) { if (tab == this.activeTab) {
this.selectTab(this.tabs[newIndex]) this.selectTab(this.tabs[newIndex])
} }
this.saveTabs() this.tabsChanged$.next()
}
saveTabs () {
window.localStorage.tabsRecovery = JSON.stringify(
this.tabs
.map((tab) => tab.getRecoveryToken())
.filter((token) => !!token)
)
}
async restoreTabs (): Promise<void> {
if (window.localStorage.tabsRecovery) {
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
let tab: Tab
for (let provider of this.tabRecoveryProviders) {
try {
tab = await provider.recover(token)
if (tab) {
break
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
if (tab) {
this.openTab(tab)
} else {
this.logger.warn('Cannot restore tab from the token:', token)
}
}
this.saveTabs()
}
} }
} }

View File

@ -0,0 +1,45 @@
import { Injectable, Inject } from '@angular/core'
import { Logger, LogService } from 'services/log'
import { BaseTabComponent } from 'components/baseTab'
import { TabRecoveryProvider } from 'api/tabRecovery'
import { AppService } from 'services/app'
@Injectable()
export class TabRecoveryService {
logger: Logger
constructor(
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
app: AppService,
log: LogService
) {
this.logger = log.create('tabRecovery')
app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs)
})
}
saveTabs (tabs: BaseTabComponent[]) {
window.localStorage.tabsRecovery = JSON.stringify(
tabs
.map((tab) => tab.getRecoveryToken())
.filter((token) => !!token)
)
}
async recoverTabs (): Promise<void> {
if (window.localStorage.tabsRecovery) {
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.tabRecoveryProviders) {
try {
await provider.recover(token)
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
}
}
}
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api' import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { SettingsTab } from './tab' import { SettingsTabComponent } from './components/settingsTab'
@Injectable() @Injectable()
@ -17,11 +17,11 @@ export class ButtonProvider extends ToolbarButtonProvider {
title: 'Settings', title: 'Settings',
weight: 10, weight: 10,
click: () => { click: () => {
let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTab) let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTabComponent)
if (settingsTab) { if (settingsTab) {
this.app.selectTab(settingsTab) this.app.selectTab(settingsTab)
} else { } else {
this.app.openTab(new SettingsTab()) this.app.openNewTab(SettingsTabComponent)
} }
} }
}] }]

View File

@ -2,22 +2,20 @@ import { Component, Inject } from '@angular/core'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
import { IHotkeyDescription, HotkeyProvider } from 'api/hotkeyProvider' import { IHotkeyDescription, HotkeyProvider, BaseTabComponent } from 'api'
import { BaseTabComponent } from 'components/baseTab'
import { SettingsTab } from '../tab'
import { SettingsTabProvider } from '../api' import { SettingsTabProvider } from '../api'
@Component({ @Component({
selector: 'settings-pane', selector: 'settings-tab',
template: require('./settingsPane.pug'), template: require('./settingsTab.pug'),
styles: [ styles: [
require('./settingsPane.scss'), require('./settingsTab.scss'),
require('./settingsPane.deep.css'), require('./settingsTab.deep.css'),
], ],
}) })
export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> { export class SettingsTabComponent extends BaseTabComponent {
globalHotkey = ['Ctrl+Shift+G'] globalHotkey = ['Ctrl+Shift+G']
private hotkeyDescriptions: IHotkeyDescription[] private hotkeyDescriptions: IHotkeyDescription[]
@ -30,6 +28,12 @@ export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> {
) { ) {
super() super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.title$.next('Settings')
this.scrollable = true
}
getRecoveryToken (): any {
return { type: 'app:settings' }
} }
ngOnDestroy () { ngOnDestroy () {

View File

@ -7,7 +7,7 @@ import { HotkeyInputComponent } from './components/hotkeyInput'
import { HotkeyDisplayComponent } from './components/hotkeyDisplay' import { HotkeyDisplayComponent } from './components/hotkeyDisplay'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal' import { HotkeyInputModalComponent } from './components/hotkeyInputModal'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput'
import { SettingsPaneComponent } from './components/settingsPane' import { SettingsTabComponent } from './components/settingsTab'
import { SettingsTabBodyComponent } from './components/settingsTabBody' import { SettingsTabBodyComponent } from './components/settingsTabBody'
import { ToolbarButtonProvider, TabRecoveryProvider } from 'api' import { ToolbarButtonProvider, TabRecoveryProvider } from 'api'
@ -28,14 +28,14 @@ import { RecoveryProvider } from './recoveryProvider'
], ],
entryComponents: [ entryComponents: [
HotkeyInputModalComponent, HotkeyInputModalComponent,
SettingsPaneComponent, SettingsTabComponent,
], ],
declarations: [ declarations: [
HotkeyDisplayComponent, HotkeyDisplayComponent,
HotkeyInputComponent, HotkeyInputComponent,
HotkeyInputModalComponent, HotkeyInputModalComponent,
MultiHotkeyInputComponent, MultiHotkeyInputComponent,
SettingsPaneComponent, SettingsTabComponent,
SettingsTabBodyComponent, SettingsTabBodyComponent,
], ],
}) })

View File

@ -1,14 +1,19 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Tab, TabRecoveryProvider } from 'api' import { TabRecoveryProvider, AppService } from 'api'
import { SettingsTab } from './tab' import { SettingsTabComponent } from './components/settingsTab'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<Tab> { constructor(
private app: AppService
) {
super()
}
async recover (recoveryToken: any): Promise<void> {
if (recoveryToken.type == 'app:settings') { if (recoveryToken.type == 'app:settings') {
return new SettingsTab() this.app.openNewTab(SettingsTabComponent)
} }
return null
} }
} }

View File

@ -1,20 +0,0 @@
import { Tab, ComponentType } from 'api/tab'
import { SettingsPaneComponent } from './components/settingsPane'
export class SettingsTab extends Tab {
constructor () {
super()
this.title = 'Settings'
this.scrollable = true
}
getComponentType (): ComponentType<SettingsTab> {
return SettingsPaneComponent
}
getRecoveryToken (): any {
return {
type: 'app:settings',
}
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService } from 'api' import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { SessionsService } from './services/sessions' import { SessionsService } from './services/sessions'
import { TerminalTab } from './tab' import { TerminalTabComponent } from './components/terminalTab'
@Injectable() @Injectable()
@ -14,17 +14,20 @@ export class ButtonProvider extends ToolbarButtonProvider {
super() super()
hotkeys.matchedHotkey.subscribe(async (hotkey) => { hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey == 'new-tab') { if (hotkey == 'new-tab') {
this.app.openTab(await this.getNewTab()) this.openNewTab()
} }
}) })
} }
async getNewTab (): Promise<TerminalTab> { async openNewTab (): Promise<void> {
let cwd = null let cwd = null
if (this.app.activeTab instanceof TerminalTab) { if (this.app.activeTab instanceof TerminalTabComponent) {
cwd = await this.app.activeTab.session.getWorkingDirectory() cwd = await this.app.activeTab.session.getWorkingDirectory()
} }
return new TerminalTab(await this.sessions.createNewSession({ command: 'zsh', cwd })) this.app.openNewTab(
TerminalTabComponent,
{ session: await this.sessions.createNewSession({ command: 'zsh', cwd }) }
)
} }
provide (): IToolbarButton[] { provide (): IToolbarButton[] {
@ -32,7 +35,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
icon: 'plus', icon: 'plus',
title: 'New terminal', title: 'New terminal',
click: async () => { click: async () => {
this.app.openTab(await this.getNewTab()) this.openNewTab()
} }
}] }]
} }

View File

@ -48,23 +48,22 @@
.col-lg-6 .col-lg-6
.form-group .form-group
label Font label Font
input.form-control( .row
type='text', .col-8
[ngbTypeahead]='fontAutocomplete', input.form-control(
'[(ngModel)]'='config.store.terminal.font', type='text',
(ngModelChange)='config.save()', [ngbTypeahead]='fontAutocomplete',
) '[(ngModel)]'='config.store.terminal.font',
(ngModelChange)='config.save()',
)
.col-4
input.form-control(
type='number',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Font to be used in the terminal small.form-text.text-muted Font to be used in the terminal
.form-group
label Font size
input.form-control(
type='number',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Text size to be used in the terminal
.form-group .form-group
label Color scheme label Color scheme
select.form-control( select.form-control(

View File

@ -1,11 +1,10 @@
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs' import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs'
import { Component, NgZone, Inject, ViewChild, HostBinding } from '@angular/core' import { Component, NgZone, Inject, ViewChild, HostBinding, Input } from '@angular/core'
import { BaseTabComponent } from 'components/baseTab'
import { TerminalTab } from '../tab'
import { TerminalDecorator, ResizeEvent } from '../api' import { TerminalDecorator, ResizeEvent } from '../api'
import { AppService, ConfigService } from 'api' import { Session } from '../services/sessions'
import { AppService, ConfigService, BaseTabComponent } from 'api'
import { hterm, preferenceManager } from '../hterm' import { hterm, preferenceManager } from '../hterm'
@ -14,17 +13,17 @@ import { hterm, preferenceManager } from '../hterm'
template: '<div #content class="content"></div>', template: '<div #content class="content"></div>',
styles: [require('./terminalTab.scss')], styles: [require('./terminalTab.scss')],
}) })
export class TerminalTabComponent extends BaseTabComponent<TerminalTab> { export class TerminalTabComponent extends BaseTabComponent {
hterm: any hterm: any
configSubscription: Subscription configSubscription: Subscription
focusedSubscription: Subscription focusedSubscription: Subscription
title$ = new BehaviorSubject('')
size$ = new ReplaySubject<ResizeEvent>(1) size$ = new ReplaySubject<ResizeEvent>(1)
input$ = new Subject<string>() input$ = new Subject<string>()
output$ = new Subject<string>() output$ = new Subject<string>()
contentUpdated$ = new Subject<void>() contentUpdated$ = new Subject<void>()
alternateScreenActive$ = new BehaviorSubject(false) alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject<Event>() mouseEvent$ = new Subject<Event>()
@Input() session: Session
@ViewChild('content') content @ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string @HostBinding('style.background-color') backgroundColor: string
private io: any private io: any
@ -41,8 +40,15 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
}) })
} }
initTab () { getRecoveryToken (): any {
this.focusedSubscription = this.model.focused.subscribe(() => { return {
type: 'app:terminal',
recoveryId: this.session.recoveryId,
}
}
ngOnInit () {
this.focusedSubscription = this.focused.subscribe(() => {
this.hterm.scrollPort_.focus() this.hterm.scrollPort_.focus()
}) })
@ -57,24 +63,24 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.hterm.installKeyboard() this.hterm.installKeyboard()
this.io = this.hterm.io.push() this.io = this.hterm.io.push()
this.attachIOHandlers(this.io) this.attachIOHandlers(this.io)
this.model.session.output$.subscribe((data) => { this.session.output$.subscribe((data) => {
this.zone.run(() => { this.zone.run(() => {
this.output$.next(data) this.output$.next(data)
}) })
this.write(data) this.write(data)
}) })
this.model.session.closed$.first().subscribe(() => { this.session.closed$.first().subscribe(() => {
this.app.closeTab(this.model) this.app.closeTab(this)
}) })
this.model.session.releaseInitialDataBuffer() this.session.releaseInitialDataBuffer()
} }
this.hterm.decorate(this.content.nativeElement) this.hterm.decorate(this.content.nativeElement)
this.configure() this.configure()
setTimeout(() => { setTimeout(() => {
this.output$.subscribe(() => { this.output$.subscribe(() => {
this.model.displayActivity() this.displayActivity()
}) })
}, 1000) }, 1000)
} }
@ -82,7 +88,6 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
attachHTermHandlers (hterm: any) { attachHTermHandlers (hterm: any) {
hterm.setWindowTitle = (title) => { hterm.setWindowTitle = (title) => {
this.zone.run(() => { this.zone.run(() => {
this.model.title = title
this.title$.next(title) this.title$.next(title)
}) })
} }
@ -142,14 +147,14 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
io.onTerminalResize = (columns, rows) => { io.onTerminalResize = (columns, rows) => {
// console.log(`Resizing to ${columns}x${rows}`) // console.log(`Resizing to ${columns}x${rows}`)
this.zone.run(() => { this.zone.run(() => {
this.model.session.resize(columns, rows) this.session.resize(columns, rows)
this.size$.next({ width: columns, height: rows }) this.size$.next({ width: columns, height: rows })
}) })
} }
} }
sendInput (data: string) { sendInput (data: string) {
this.model.session.write(data) this.session.write(data)
} }
write (data: string) { write (data: string) {
@ -198,5 +203,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.contentUpdated$.complete() this.contentUpdated$.complete()
this.alternateScreenActive$.complete() this.alternateScreenActive$.complete()
this.mouseEvent$.complete() this.mouseEvent$.complete()
this.session.gracefullyDestroy()
} }
} }

View File

@ -1,23 +1,25 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Tab, TabRecoveryProvider } from 'api' import { TabRecoveryProvider, AppService } from 'api'
import { TerminalTab } from './tab'
import { SessionsService } from './services/sessions' import { SessionsService } from './services/sessions'
import { TerminalTabComponent } from './components/terminalTab'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
constructor (private sessions: SessionsService) { constructor (
private sessions: SessionsService,
private app: AppService,
) {
super() super()
} }
async recover (recoveryToken: any): Promise<Tab> { async recover (recoveryToken: any): Promise<void> {
if (recoveryToken.type == 'app:terminal') { if (recoveryToken.type == 'app:terminal') {
let session = await this.sessions.recover(recoveryToken.recoveryId) let session = await this.sessions.recover(recoveryToken.recoveryId)
if (!session) { if (!session) {
return null return
} }
return new TerminalTab(session) this.app.openNewTab(TerminalTabComponent, { session })
} }
return null
} }
} }

View File

@ -1,27 +0,0 @@
import { Tab, ComponentType } from 'api/tab'
import { TerminalTabComponent } from './components/terminalTab'
import { Session } from './services/sessions'
export class TerminalTab extends Tab {
static recoveryId = 'app:terminal'
constructor (public session: Session) {
super()
}
getComponentType (): ComponentType<TerminalTab> {
return TerminalTabComponent
}
getRecoveryToken (): any {
return {
type: 'app:terminal',
recoveryId: this.session.recoveryId,
}
}
destroy (): void {
this.session.gracefullyDestroy()
}
}

View File

@ -135,7 +135,7 @@ app-root > .content {
margin-top: 3px; margin-top: 3px;
tab-header { tab-header {
&.pre-selected, &:nth-last-child(1) { &.pre-selected {
.content-wrapper { .content-wrapper {
border-bottom-right-radius: $tab-border-radius; border-bottom-right-radius: $tab-border-radius;
} }
@ -167,7 +167,7 @@ app-root > .content {
margin-bottom: 3px; margin-bottom: 3px;
tab-header { tab-header {
&.pre-selected, &:nth-last-child(1) { &.pre-selected {
.content-wrapper { .content-wrapper {
border-top-right-radius: $tab-border-radius; border-top-right-radius: $tab-border-radius;
} }
@ -200,7 +200,7 @@ tab-body {
background: $body-bg; background: $body-bg;
} }
settings-pane > ngb-tabset { settings-tab > ngb-tabset {
border-right: 1px solid $body-bg2; border-right: 1px solid $body-bg2;
& > .nav { & > .nav {