diff --git a/tabby-serial/src/components/serialTab.component.ts b/tabby-serial/src/components/serialTab.component.ts index 3e332f81..7ff0afb1 100644 --- a/tabby-serial/src/components/serialTab.component.ts +++ b/tabby-serial/src/components/serialTab.component.ts @@ -2,9 +2,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' import colors from 'ansi-colors' import { Component, Injector } from '@angular/core' -import { first } from 'rxjs' -import { GetRecoveryTokenOptions, Platform, SelectorService } from 'tabby-core' -import { BaseTerminalTabComponent, Reconnectable } from 'tabby-terminal' +import { Platform, SelectorService } from 'tabby-core' +import { BaseTerminalTabComponent, ConnectableTerminalTabComponent } from 'tabby-terminal' import { SerialSession, BAUD_RATES, SerialProfile } from '../api' /** @hidden */ @@ -14,7 +13,7 @@ import { SerialSession, BAUD_RATES, SerialProfile } from '../api' styleUrls: ['./serialTab.component.scss', ...BaseTerminalTabComponent.styles], animations: BaseTerminalTabComponent.animations, }) -export class SerialTabComponent extends BaseTerminalTabComponent implements Reconnectable { +export class SerialTabComponent extends ConnectableTerminalTabComponent { session: SerialSession|null = null Platform = Platform @@ -28,8 +27,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent } ngOnInit () { - this.logger = this.log.create('terminalTab') - this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { if (!this.hasFocus) { return @@ -54,12 +51,9 @@ export class SerialTabComponent extends BaseTerminalTabComponent }) } - protected onFrontendReady (): void { - this.initializeSession() - super.onFrontendReady() - } - async initializeSession () { + super.initializeSession() + const session = new SerialSession(this.injector, this.profile) this.setSession(session) @@ -82,38 +76,16 @@ export class SerialTabComponent extends BaseTerminalTabComponent this.write(`\r\n${colors.black.bgWhite(' Serial ')} ${msg}\r\n`) this.session?.resize(this.size.columns, this.size.rows) }) - this.attachSessionHandler(this.session!.destroyed$, () => { - if (this.frontend) { - // Session was closed abruptly - this.write('\r\n' + colors.black.bgWhite(' SERIAL ') + ` session closed\r\n`) - - if (this.profile.behaviorOnSessionEnd === 'reconnect') { - this.reconnect() - } else if (this.profile.behaviorOnSessionEnd === 'keep' || this.profile.behaviorOnSessionEnd === 'auto' && !this.isSessionExplicitlyTerminated()) { - this.write(this.translate.instant(_('Press any key to reconnect')) + '\r\n') - this.input$.pipe(first()).subscribe(() => { - if (!this.session?.open) { - this.reconnect() - } - }) - } - } - }) super.attachSessionHandlers() } - async getRecoveryToken (options?: GetRecoveryTokenOptions): Promise { - return { - type: 'app:serial-tab', - profile: this.profile, - savedState: options?.includeState && this.frontend?.saveState(), - } - } + protected onSessionDestroyed (): void { + if (this.frontend) { + // Session was closed abruptly + this.write('\r\n' + colors.black.bgWhite(' SERIAL ') + ` session closed\r\n`) - async reconnect (): Promise { - this.session?.destroy() - await this.initializeSession() - this.session?.releaseInitialDataBuffer() + super.onSessionDestroyed() + } } async changeBaudRate () { diff --git a/tabby-ssh/src/components/sshTab.component.ts b/tabby-ssh/src/components/sshTab.component.ts index 97bc2da5..b6c6d3eb 100644 --- a/tabby-ssh/src/components/sshTab.component.ts +++ b/tabby-ssh/src/components/sshTab.component.ts @@ -2,9 +2,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' import colors from 'ansi-colors' import { Component, Injector, HostListener } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { first } from 'rxjs' -import { GetRecoveryTokenOptions, Platform, ProfilesService, RecoveryToken } from 'tabby-core' -import { BaseTerminalTabComponent, Reconnectable } from 'tabby-terminal' +import { Platform, ProfilesService } from 'tabby-core' +import { BaseTerminalTabComponent, ConnectableTerminalTabComponent } from 'tabby-terminal' import { SSHService } from '../services/ssh.service' import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh' import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component' @@ -22,7 +21,7 @@ import { SSHMultiplexerService } from '../services/sshMultiplexer.service' ], animations: BaseTerminalTabComponent.animations, }) -export class SSHTabComponent extends BaseTerminalTabComponent implements Reconnectable { +export class SSHTabComponent extends ConnectableTerminalTabComponent { Platform = Platform sshSession: SSHSession|null = null session: SSHShellSession|null = null @@ -30,7 +29,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem sftpPath = '/' enableToolbar = true activeKIPrompt: KeyboardInteractivePrompt|null = null - private reconnectOffered = false constructor ( injector: Injector, @@ -46,8 +44,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem } ngOnInit (): void { - this.logger = this.log.create('terminalTab') - this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { if (!this.hasFocus) { return @@ -73,11 +69,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem super.ngOnInit() } - protected onFrontendReady (): void { - this.initializeSession() - super.onFrontendReady() - } - async setupOneSession (injector: Injector, profile: SSHProfile, multiplex = true): Promise { let session = await this.sshMultiplexer.getSession(profile) if (!multiplex || !session || !profile.options.reuseSession) { @@ -150,29 +141,13 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem return session } - protected attachSessionHandlers (): void { - const session = this.session! - this.attachSessionHandler(session.destroyed$, () => { - if (this.frontend) { - // Session was closed abruptly - this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` ${this.sshSession?.profile.options.host}: session closed\r\n`) + protected onSessionDestroyed (): void { + if (this.frontend) { + // Session was closed abruptly + this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` ${this.sshSession?.profile.options.host}: session closed\r\n`) - if (this.profile.behaviorOnSessionEnd === 'reconnect') { - this.reconnect() - } else if (this.profile.behaviorOnSessionEnd === 'keep' || this.profile.behaviorOnSessionEnd === 'auto' && !this.isSessionExplicitlyTerminated()) { - if (!this.reconnectOffered) { - this.reconnectOffered = true - this.write(this.translate.instant(_('Press any key to reconnect')) + '\r\n') - this.input$.pipe(first()).subscribe(() => { - if (!this.session?.open && this.reconnectOffered) { - this.reconnect() - } - }) - } - } - } - }) - super.attachSessionHandlers() + super.onSessionDestroyed() + } } private async initializeSessionMaybeMultiplex (multiplex = true): Promise { @@ -196,7 +171,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem } async initializeSession (): Promise { - this.reconnectOffered = false + await super.initializeSession() try { await this.initializeSessionMaybeMultiplex(true) } catch { @@ -209,25 +184,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem } } - async getRecoveryToken (options?: GetRecoveryTokenOptions): Promise { - return { - type: 'app:ssh-tab', - profile: this.profile, - savedState: options?.includeState && this.frontend?.saveState(), - } - } - showPortForwarding (): void { const modal = this.ngbModal.open(SSHPortForwardingModalComponent).componentInstance as SSHPortForwardingModalComponent modal.session = this.sshSession! } - async reconnect (): Promise { - this.session?.destroy() - await this.initializeSession() - this.session?.releaseInitialDataBuffer() - } - async canClose (): Promise { if (!this.session?.open) { return true diff --git a/tabby-telnet/src/components/telnetTab.component.ts b/tabby-telnet/src/components/telnetTab.component.ts index d3ae2cf4..3b785bd5 100644 --- a/tabby-telnet/src/components/telnetTab.component.ts +++ b/tabby-telnet/src/components/telnetTab.component.ts @@ -1,9 +1,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' import colors from 'ansi-colors' import { Component, Injector } from '@angular/core' -import { first } from 'rxjs' -import { GetRecoveryTokenOptions, Platform, RecoveryToken } from 'tabby-core' -import { BaseTerminalTabComponent, Reconnectable } from 'tabby-terminal' +import { Platform } from 'tabby-core' +import { BaseTerminalTabComponent, ConnectableTerminalTabComponent } from 'tabby-terminal' import { TelnetProfile, TelnetSession } from '../session' @@ -14,10 +13,9 @@ import { TelnetProfile, TelnetSession } from '../session' styleUrls: ['./telnetTab.component.scss', ...BaseTerminalTabComponent.styles], animations: BaseTerminalTabComponent.animations, }) -export class TelnetTabComponent extends BaseTerminalTabComponent implements Reconnectable { +export class TelnetTabComponent extends ConnectableTerminalTabComponent { Platform = Platform session: TelnetSession|null = null - private reconnectOffered = false // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor ( @@ -28,8 +26,6 @@ export class TelnetTabComponent extends BaseTerminalTabComponent } ngOnInit (): void { - this.logger = this.log.create('telnetTab') - this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { if (this.hasFocus && hotkey === 'restart-telnet-session') { this.reconnect() @@ -39,38 +35,17 @@ export class TelnetTabComponent extends BaseTerminalTabComponent super.ngOnInit() } - protected onFrontendReady (): void { - this.initializeSession() - super.onFrontendReady() - } + protected onSessionDestroyed (): void { + if (this.frontend) { + // Session was closed abruptly + this.write('\r\n' + colors.black.bgWhite(' TELNET ') + ` ${this.session?.profile.options.host}: session closed\r\n`) - protected attachSessionHandlers (): void { - const session = this.session! - this.attachSessionHandler(session.destroyed$, () => { - if (this.frontend) { - // Session was closed abruptly - this.write('\r\n' + colors.black.bgWhite(' TELNET ') + ` ${this.session?.profile.options.host}: session closed\r\n`) - - if (this.profile.behaviorOnSessionEnd === 'reconnect') { - this.reconnect() - } else if (this.profile.behaviorOnSessionEnd === 'keep' || this.profile.behaviorOnSessionEnd === 'auto' && !this.isSessionExplicitlyTerminated()) { - if (!this.reconnectOffered) { - this.reconnectOffered = true - this.write(this.translate.instant(_('Press any key to reconnect')) + '\r\n') - this.input$.pipe(first()).subscribe(() => { - if (!this.session?.open && this.reconnectOffered) { - this.reconnect() - } - }) - } - } - } - }) - super.attachSessionHandlers() + super.onSessionDestroyed() + } } async initializeSession (): Promise { - this.reconnectOffered = false + await super.initializeSession() const session = new TelnetSession(this.injector, this.profile) this.setSession(session) @@ -96,20 +71,6 @@ export class TelnetTabComponent extends BaseTerminalTabComponent } } - async getRecoveryToken (options?: GetRecoveryTokenOptions): Promise { - return { - type: 'app:telnet-tab', - profile: this.profile, - savedState: options?.includeState && this.frontend?.saveState(), - } - } - - async reconnect (): Promise { - this.session?.destroy() - await this.initializeSession() - this.session?.releaseInitialDataBuffer() - } - async canClose (): Promise { if (!this.session?.open) { return true diff --git a/tabby-terminal/src/api/baseTerminalTab.component.ts b/tabby-terminal/src/api/baseTerminalTab.component.ts index dcea967d..a5285293 100644 --- a/tabby-terminal/src/api/baseTerminalTab.component.ts +++ b/tabby-terminal/src/api/baseTerminalTab.component.ts @@ -9,7 +9,7 @@ import { BaseSession } from '../session' import { Frontend } from '../frontends/frontend' import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend' -import { ResizeEvent, BaseTerminalProfile, isReconnectable } from './interfaces' +import { ResizeEvent, BaseTerminalProfile } from './interfaces' import { TerminalDecorator } from './decorator' import { SearchPanelComponent } from '../components/searchPanel.component' import { MultifocusService } from '../services/multifocus.service' @@ -312,11 +312,6 @@ export class BaseTerminalTabComponent

extends Bas case 'scroll-to-bottom': this.frontend?.scrollToBottom() break - case 'reconnect-tab': - if (isReconnectable(this)) { - this.reconnect() - } - break } }) @@ -784,7 +779,7 @@ export class BaseTerminalTabComponent

extends Bas }) this.attachSessionHandler(this.session.destroyed$, () => { - this.setSession(null) + this.onSessionDestroyed() }) this.attachSessionHandler(this.session.oscProcessor.copyRequested$, content => { @@ -793,6 +788,13 @@ export class BaseTerminalTabComponent

extends Bas }) } + /** + * Method called when session is destroyed. Set the session to null + */ + protected onSessionDestroyed (): void { + this.setSession(null) + } + protected detachSessionHandlers (): void { this.sessionHandlers.cancelAll() } diff --git a/tabby-terminal/src/api/connectableTerminalTab.component.ts b/tabby-terminal/src/api/connectableTerminalTab.component.ts new file mode 100644 index 00000000..0d63ff2d --- /dev/null +++ b/tabby-terminal/src/api/connectableTerminalTab.component.ts @@ -0,0 +1,94 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' + +import { Injector, Component } from '@angular/core' + +import { first } from 'rxjs' + +import { BaseTerminalProfile } from './interfaces' +import { BaseTerminalTabComponent } from './baseTerminalTab.component' +import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core' + + +/** + * A class to base your custom connectable terminal tabs on + */ +@Component({ template: '' }) +export abstract class ConnectableTerminalTabComponent

extends BaseTerminalTabComponent

{ + + protected reconnectOffered = false + + constructor (protected injector: Injector) { + super(injector) + + this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { + if (this.hasFocus && hotkey === 'reconnect-tab') { + this.reconnect() + } + }) + } + + ngOnInit (): void { + this.logger = this.log.create(`${this.profile.type}Tab`) + + super.ngOnInit() + } + + protected onFrontendReady (): void { + this.initializeSession() + super.onFrontendReady() + } + + /** + * Initialize Connectable Session. + * Set reconnectOffered to false + */ + async initializeSession (): Promise { + this.reconnectOffered = false + } + + /** + * Method called when session is destroyed. Handle the tab behavior on session end for connectable tab + */ + protected onSessionDestroyed (): void { + super.onSessionDestroyed() + + if (this.frontend) { + if (this.profile.behaviorOnSessionEnd === 'reconnect') { + this.reconnect() + } else if (this.profile.behaviorOnSessionEnd === 'keep' || this.profile.behaviorOnSessionEnd === 'auto' && !this.isSessionExplicitlyTerminated()) { + this.offerReconnection() + } + } + } + + /** + * Offering reconnection to the user if it hasn't been done yet. + * Set reconnectOffered to true + */ + offerReconnection (): void { + if (!this.reconnectOffered) { + this.reconnectOffered = true + this.write(this.translate.instant(_('Press any key to reconnect')) + '\r\n') + this.input$.pipe(first()).subscribe(() => { + if (!this.session?.open && this.reconnectOffered) { + this.reconnect() + } + }) + } + } + + async getRecoveryToken (options?: GetRecoveryTokenOptions): Promise { + return { + type: `app:${this.profile.type}-tab`, + profile: this.profile, + savedState: options?.includeState && this.frontend?.saveState(), + } + } + + async reconnect (): Promise { + this.session?.destroy() + await this.initializeSession() + this.session?.releaseInitialDataBuffer() + } + +} diff --git a/tabby-terminal/src/api/interfaces.ts b/tabby-terminal/src/api/interfaces.ts index 8e7d54c3..74143339 100644 --- a/tabby-terminal/src/api/interfaces.ts +++ b/tabby-terminal/src/api/interfaces.ts @@ -19,12 +19,3 @@ export interface TerminalColorScheme { export interface BaseTerminalProfile extends Profile { terminalColorScheme?: TerminalColorScheme } - -export interface Reconnectable { - reconnect: () => Promise; -} - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function isReconnectable (object: any): object is Reconnectable { - return 'reconnect' in object -} diff --git a/tabby-terminal/src/index.ts b/tabby-terminal/src/index.ts index f6706875..35b0ac70 100644 --- a/tabby-terminal/src/index.ts +++ b/tabby-terminal/src/index.ts @@ -90,6 +90,7 @@ export default class TerminalModule { } // eslint-disable-line @typescript-eslin export { TerminalDecorator, TerminalContextMenuItemProvider, TerminalColorSchemeProvider } export { Frontend, XTermFrontend, XTermWebGLFrontend } export { BaseTerminalTabComponent } from './api/baseTerminalTab.component' +export { ConnectableTerminalTabComponent } from './api/connectableTerminalTab.component' export * from './api/interfaces' export * from './middleware/streamProcessing' export * from './middleware/loginScriptProcessing' diff --git a/tabby-terminal/src/tabContextMenu.ts b/tabby-terminal/src/tabContextMenu.ts index 962c12e4..5d485022 100644 --- a/tabby-terminal/src/tabContextMenu.ts +++ b/tabby-terminal/src/tabContextMenu.ts @@ -1,9 +1,9 @@ import { Injectable, Optional, Inject } from '@angular/core' import { BaseTabComponent, TabContextMenuItemProvider, NotificationsService, MenuItemOptions, TranslateService, SplitTabComponent } from 'tabby-core' import { BaseTerminalTabComponent } from './api/baseTerminalTab.component' -import { isReconnectable } from './api/interfaces' import { TerminalContextMenuItemProvider } from './api/contextMenuProvider' import { MultifocusService } from './services/multifocus.service' +import { ConnectableTerminalTabComponent } from './api/connectableTerminalTab.component' /** @hidden */ @Injectable() @@ -97,7 +97,7 @@ export class ReconnectContextMenu extends TabContextMenuItemProvider { ) { super() } async getItems (tab: BaseTabComponent): Promise { - if (isReconnectable(tab)) { + if (tab instanceof ConnectableTerminalTabComponent) { return [ { label: this.translate.instant('Reconnect'),