diff --git a/tabby-core/src/api/profileProvider.ts b/tabby-core/src/api/profileProvider.ts index 8fdbc2e6..a6e6bdd6 100644 --- a/tabby-core/src/api/profileProvider.ts +++ b/tabby-core/src/api/profileProvider.ts @@ -14,6 +14,7 @@ export interface Profile { icon?: string color?: string disableDynamicTitle: boolean + behaviorOnSessionEnd: 'auto'|'keep'|'reconnect'|'close' weight: number isBuiltin: boolean diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index 97d3488d..ef26e683 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/tabby-core/src/services/profiles.service.ts @@ -24,6 +24,7 @@ export class ProfilesService { isBuiltin: false, isTemplate: false, terminalColorScheme: null, + behaviorOnSessionEnd: 'auto', } constructor ( diff --git a/tabby-local/src/components/terminalTab.component.ts b/tabby-local/src/components/terminalTab.component.ts index 2f256502..b14d03b6 100644 --- a/tabby-local/src/components/terminalTab.component.ts +++ b/tabby-local/src/components/terminalTab.component.ts @@ -71,7 +71,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent height: rows, }) - this.setSession(session, true) + this.setSession(session) this.recoveryStateChangedHint.next() } @@ -127,4 +127,12 @@ export class TerminalTabComponent extends BaseTerminalTabComponent super.ngOnDestroy() this.session?.destroy() } + + /** + * Return true if the user explicitly exit the session. + * Always return true for terminalTab as the session can only be ended by the user + */ + protected isSessionExplicitlyTerminated (): boolean { + return true + } } diff --git a/tabby-serial/src/components/serialTab.component.ts b/tabby-serial/src/components/serialTab.component.ts index f9293be4..3e332f81 100644 --- a/tabby-serial/src/components/serialTab.component.ts +++ b/tabby-serial/src/components/serialTab.component.ts @@ -83,12 +83,21 @@ export class SerialTabComponent extends BaseTerminalTabComponent this.session?.resize(this.size.columns, this.size.rows) }) this.attachSessionHandler(this.session!.destroyed$, () => { - this.write(this.translate.instant(_('Press any key to reconnect')) + '\r\n') - this.input$.pipe(first()).subscribe(() => { - if (!this.session?.open) { + 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() } @@ -117,4 +126,10 @@ export class SerialTabComponent extends BaseTerminalTabComponent this.session?.serial?.update({ baudRate: rate }) this.profile.options.baudrate = rate } + + protected isSessionExplicitlyTerminated (): boolean { + return super.isSessionExplicitlyTerminated() || + this.recentInputs.endsWith('close\r') || + this.recentInputs.endsWith('quit\r') + } } diff --git a/tabby-settings/src/components/editProfileModal.component.pug b/tabby-settings/src/components/editProfileModal.component.pug index 2a70ce5a..7869099b 100644 --- a/tabby-settings/src/components/editProfileModal.component.pug +++ b/tabby-settings/src/components/editProfileModal.component.pug @@ -65,6 +65,18 @@ .description(translate) Connection name will be used instead toggle([(ngModel)]='profile.disableDynamicTitle') + .form-line + .header + .title(translate) When a session ends + .description(*ngIf='profile.behaviorOnSessionEnd == "auto"', translate) Only close the tab when session is explicitly terminated + select.form-control( + [(ngModel)]='profile.behaviorOnSessionEnd', + ) + option(ngValue='auto', translate) Auto + option(ngValue='keep', translate) Keep + option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect + option(ngValue='close', translate) Close + .mb-4 .col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent') diff --git a/tabby-ssh/src/components/sshTab.component.ts b/tabby-ssh/src/components/sshTab.component.ts index ea2df932..97bc2da5 100644 --- a/tabby-ssh/src/components/sshTab.component.ts +++ b/tabby-ssh/src/components/sshTab.component.ts @@ -153,24 +153,22 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem protected attachSessionHandlers (): void { const session = this.session! this.attachSessionHandler(session.destroyed$, () => { - if ( - // Ctrl-D - this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 || - this.recentInputs.endsWith('exit\r') - ) { - // User closed the session - this.destroy() - } else if (this.frontend) { + 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.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() - } - }) + + 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() + } + }) + } } } }) @@ -262,4 +260,10 @@ export class SSHTabComponent extends BaseTerminalTabComponent implem onClick (): void { this.sftpPanelVisible = false } + + protected isSessionExplicitlyTerminated (): boolean { + return super.isSessionExplicitlyTerminated() || + this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 || + this.recentInputs.endsWith('exit\r') + } } diff --git a/tabby-telnet/src/components/telnetTab.component.ts b/tabby-telnet/src/components/telnetTab.component.ts index ac9cf429..d3ae2cf4 100644 --- a/tabby-telnet/src/components/telnetTab.component.ts +++ b/tabby-telnet/src/components/telnetTab.component.ts @@ -49,14 +49,20 @@ export class TelnetTabComponent extends BaseTerminalTabComponent this.attachSessionHandler(session.destroyed$, () => { if (this.frontend) { // Session was closed abruptly - 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() - } - }) + 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() + } + }) + } } } }) @@ -121,4 +127,11 @@ export class TelnetTabComponent extends BaseTerminalTabComponent }, )).response === 0 } + + protected isSessionExplicitlyTerminated (): boolean { + return super.isSessionExplicitlyTerminated() || + this.recentInputs.endsWith('close\r') || + this.recentInputs.endsWith('quit\r') + } + } diff --git a/tabby-terminal/src/api/baseTerminalTab.component.ts b/tabby-terminal/src/api/baseTerminalTab.component.ts index bfc406ad..f6a5a619 100644 --- a/tabby-terminal/src/api/baseTerminalTab.component.ts +++ b/tabby-terminal/src/api/baseTerminalTab.component.ts @@ -771,11 +771,12 @@ export class BaseTerminalTabComponent

extends Bas } }) - if (destroyOnSessionClose) { - this.attachSessionHandler(this.session.closed$, () => { + this.attachSessionHandler(this.session.closed$, () => { + const behavior = this.profile.behaviorOnSessionEnd + if (destroyOnSessionClose || behavior === 'close' || behavior === 'auto' && this.isSessionExplicitlyTerminated()) { this.destroy() - }) - } + } + }) this.attachSessionHandler(this.session.destroyed$, () => { this.setSession(null) @@ -841,4 +842,11 @@ export class BaseTerminalTabComponent

extends Bas cb(this) } } + + /** + * Return true if the user explicitly exit the session + */ + protected isSessionExplicitlyTerminated (): boolean { + return false + } }