diff --git a/app/src/entry.ts b/app/src/entry.ts index 75073e61..f0671079 100644 --- a/app/src/entry.ts +++ b/app/src/entry.ts @@ -21,7 +21,7 @@ if (process.platform === 'win32' && !('HOME' in process.env)) { process.env.HOME = `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` } -if (process.env.TERMINUS_DEV) { +if (process.env.TERMINUS_DEV && !process.env.TERMINUS_FORCE_ANGULAR_PROD) { console.warn('Running in debug mode') } else { enableProdMode() diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index 47b8acac..53412666 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -1,3 +1,4 @@ +export { BaseComponent, SubscriptionContainer } from '../components/base.component' export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component' export { TabHeaderComponent } from '../components/tabHeader.component' export { SplitTabComponent, SplitContainer } from '../components/splitTab.component' diff --git a/terminus-core/src/components/base.component.ts b/terminus-core/src/components/base.component.ts new file mode 100644 index 00000000..cc93a556 --- /dev/null +++ b/terminus-core/src/components/base.component.ts @@ -0,0 +1,54 @@ +import { Observable, Subscription } from 'rxjs' + +interface CancellableEvent { + element: HTMLElement + event: string + handler: EventListenerOrEventListenerObject + options?: boolean|AddEventListenerOptions +} + +export class SubscriptionContainer { + private subscriptions: Subscription[] = [] + private events: CancellableEvent[] = [] + + addEventListener (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void { + element.addEventListener(event, handler, options) + this.events.push({ + element, + event, + handler, + options, + }) + } + + subscribe (observable: Observable, handler: (v: T) => void): void { + this.subscriptions.push(observable.subscribe(handler)) + } + + cancelAll (): void { + for (const s of this.subscriptions) { + s.unsubscribe() + } + for (const e of this.events) { + e.element.removeEventListener(e.event, e.handler, e.options) + } + this.subscriptions = [] + this.events = [] + } +} + +export class BaseComponent { + private subscriptionContainer = new SubscriptionContainer() + + addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void { + this.subscriptionContainer.addEventListener(element, event, handler, options) + } + + subscribeUntilDestroyed (observable: Observable, handler: (v: T) => void): void { + this.subscriptionContainer.subscribe(observable, handler) + } + + ngOnDestroy (): void { + this.subscriptionContainer.cancelAll() + } +} diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts index fea8b950..6360bc3f 100644 --- a/terminus-core/src/components/baseTab.component.ts +++ b/terminus-core/src/components/baseTab.component.ts @@ -1,6 +1,7 @@ import { Observable, Subject } from 'rxjs' import { ViewRef } from '@angular/core' import { RecoveryToken } from '../api/tabRecovery' +import { BaseComponent } from './base.component' /** * Represents an active "process" inside a tab, @@ -13,7 +14,7 @@ export interface BaseTabProcess { /** * Abstract base class for custom tab components */ -export abstract class BaseTabComponent { +export abstract class BaseTabComponent extends BaseComponent { /** * Parent tab (usually a SplitTabComponent) */ @@ -69,6 +70,7 @@ export abstract class BaseTabComponent { get recoveryStateChangedHint$ (): Observable { return this.recoveryStateChangedHint } protected constructor () { + super() this.focused$.subscribe(() => { this.hasFocus = true }) @@ -158,10 +160,17 @@ export abstract class BaseTabComponent { this.blurred.complete() this.titleChange.complete() this.progress.complete() + this.activity.complete() this.recoveryStateChangedHint.complete() if (!skipDestroyedEvent) { this.destroyed.next() } this.destroyed.complete() } + + /** @hidden */ + ngOnDestroy (): void { + this.destroy() + super.ngOnDestroy() + } } diff --git a/terminus-core/src/components/splitTab.component.ts b/terminus-core/src/components/splitTab.component.ts index 58839156..9a292656 100644 --- a/terminus-core/src/components/splitTab.component.ts +++ b/terminus-core/src/components/splitTab.component.ts @@ -1,4 +1,4 @@ -import { Observable, Subject, Subscription } from 'rxjs' +import { Observable, Subject } from 'rxjs' import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core' import { BaseTabComponent, BaseTabProcess } from './baseTab.component' import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery' @@ -163,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit /** @hidden */ private focusedTab: BaseTabComponent|null = null private maximizedTab: BaseTabComponent|null = null - private hotkeysSubscription: Subscription private viewRefs: Map> = new Map() private tabAdded = new Subject() @@ -210,7 +209,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit }) this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred())) - this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => { if (!this.hasFocus || !this.focusedTab) { return } @@ -272,7 +271,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit /** @hidden */ ngOnDestroy (): void { - this.hotkeysSubscription.unsubscribe() + this.tabAdded.complete() + this.tabRemoved.complete() + super.ngOnDestroy() } /** @returns Flat list of all sub-tabs */ @@ -497,18 +498,18 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion this.viewRefs.set(tab, ref) - ref.rootNodes[0].addEventListener('click', () => this.focus(tab)) + tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab)) - tab.titleChange$.subscribe(t => this.setTitle(t)) - tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity()) - tab.progress$.subscribe(p => this.setProgress(p)) + tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t)) + tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity()) + tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p)) if (tab.title) { this.setTitle(tab.title) } - tab.recoveryStateChangedHint$.subscribe(() => { + tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => { this.recoveryStateChangedHint.next() }) - tab.destroyed$.subscribe(() => { + tab.subscribeUntilDestroyed(tab.destroyed$, () => { this.removeTab(tab) }) } diff --git a/terminus-core/src/components/tabHeader.component.ts b/terminus-core/src/components/tabHeader.component.ts index 273507bb..625f090f 100644 --- a/terminus-core/src/components/tabHeader.component.ts +++ b/terminus-core/src/components/tabHeader.component.ts @@ -11,6 +11,7 @@ import { ElectronService } from '../services/electron.service' import { AppService } from '../services/app.service' import { HostAppService, Platform } from '../services/hostApp.service' import { ConfigService } from '../services/config.service' +import { BaseComponent } from './base.component' /** @hidden */ export interface SortableComponentProxy { @@ -23,7 +24,7 @@ export interface SortableComponentProxy { template: require('./tabHeader.component.pug'), styles: [require('./tabHeader.component.scss')], }) -export class TabHeaderComponent { +export class TabHeaderComponent extends BaseComponent { @Input() index: number @Input() @HostBinding('class.active') active: boolean @Input() tab: BaseTabComponent @@ -41,7 +42,8 @@ export class TabHeaderComponent { @Inject(SortableComponent) private parentDraggable: SortableComponentProxy, @Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[], ) { - this.hotkeys.matchedHotkey.subscribe((hotkey) => { + super() + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, (hotkey) => { if (this.app.activeTab === this.tab) { if (hotkey === 'rename-tab') { this.showRenameTabModal() @@ -52,7 +54,7 @@ export class TabHeaderComponent { } ngOnInit () { - this.tab.progress$.subscribe(progress => { + this.subscribeUntilDestroyed(this.tab.progress$, progress => { this.zone.run(() => { this.progress = progress }) diff --git a/terminus-core/src/services/hotkeys.service.ts b/terminus-core/src/services/hotkeys.service.ts index 37fdeae5..5d90a950 100644 --- a/terminus-core/src/services/hotkeys.service.ts +++ b/terminus-core/src/services/hotkeys.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core' import { Observable, Subject } from 'rxjs' import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider' -import { stringifyKeySequence } from './hotkeys.util' +import { stringifyKeySequence, EventData } from './hotkeys.util' import { ConfigService } from './config.service' import { ElectronService } from './electron.service' import { HostAppService } from './hostApp.service' @@ -14,10 +14,6 @@ export interface PartialHotkeyMatch { const KEY_TIMEOUT = 2000 -interface EventBufferEntry { - event: KeyboardEvent - time: number -} @Injectable({ providedIn: 'root' }) export class HotkeysService { @@ -32,7 +28,7 @@ export class HotkeysService { get hotkey$ (): Observable { return this._hotkey } private _hotkey = new Subject() - private currentKeystrokes: EventBufferEntry[] = [] + private currentKeystrokes: EventData[] = [] private disabledLevel = 0 private hotkeyDescriptions: HotkeyDescription[] = [] @@ -73,7 +69,16 @@ export class HotkeysService { */ pushKeystroke (name: string, nativeEvent: KeyboardEvent): void { (nativeEvent as any).event = name - this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() }) + this.currentKeystrokes.push({ + ctrlKey: nativeEvent.ctrlKey, + metaKey: nativeEvent.metaKey, + altKey: nativeEvent.altKey, + shiftKey: nativeEvent.shiftKey, + code: nativeEvent.code, + key: nativeEvent.key, + eventName: name, + time: performance.now(), + }) } /** @@ -104,7 +109,7 @@ export class HotkeysService { getCurrentKeystrokes (): string[] { this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT) - return stringifyKeySequence(this.currentKeystrokes.map(x => x.event)) + return stringifyKeySequence(this.currentKeystrokes) } getCurrentFullyMatchedHotkey (): string|null { diff --git a/terminus-core/src/services/hotkeys.util.ts b/terminus-core/src/services/hotkeys.util.ts index 35de3514..d91ebcdc 100644 --- a/terminus-core/src/services/hotkeys.util.ts +++ b/terminus-core/src/services/hotkeys.util.ts @@ -10,15 +10,26 @@ export const altKeyName = { linux: 'Alt', }[process.platform] +export interface EventData { + ctrlKey: boolean + metaKey: boolean + altKey: boolean + shiftKey: boolean + key: string + code: string + eventName: string + time: number +} + const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/ -export function stringifyKeySequence (events: KeyboardEvent[]): string[] { +export function stringifyKeySequence (events: EventData[]): string[] { const items: string[] = [] events = events.slice() while (events.length > 0) { const event = events.shift()! - if ((event as any).event === 'keydown') { + if (event.eventName === 'keydown') { const itemKeys: string[] = [] if (event.ctrlKey) { itemKeys.push('Ctrl') diff --git a/terminus-serial/src/components/serialTab.component.ts b/terminus-serial/src/components/serialTab.component.ts index ef89f7a1..bdd4e635 100644 --- a/terminus-serial/src/components/serialTab.component.ts +++ b/terminus-serial/src/components/serialTab.component.ts @@ -6,7 +6,6 @@ import { first } from 'rxjs/operators' import { BaseTerminalTabComponent } from 'terminus-terminal' import { SerialService } from '../services/serial.service' import { SerialConnection, SerialSession, BAUD_RATES } from '../api' -import { Subscription } from 'rxjs' /** @hidden */ @Component({ @@ -20,7 +19,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent { session: SerialSession|null = null serialPort: any private serialService: SerialService - private homeEndSubscription: Subscription // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor ( @@ -33,7 +31,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent { ngOnInit () { this.logger = this.log.create('terminalTab') - this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => { if (!this.hasFocus) { return } @@ -130,9 +128,4 @@ export class SerialTabComponent extends BaseTerminalTabComponent { this.serialPort.update({ baudRate: rate }) this.connection!.baudrate = rate } - - ngOnDestroy () { - this.homeEndSubscription.unsubscribe() - super.ngOnDestroy() - } } diff --git a/terminus-settings/src/components/hotkeyInputModal.component.ts b/terminus-settings/src/components/hotkeyInputModal.component.ts index cb145579..6cc9991b 100644 --- a/terminus-settings/src/components/hotkeyInputModal.component.ts +++ b/terminus-settings/src/components/hotkeyInputModal.component.ts @@ -1,8 +1,7 @@ import { Component, Input } from '@angular/core' import { trigger, transition, style, animate } from '@angular/animations' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' -import { Subscription } from 'rxjs' -import { HotkeysService } from 'terminus-core' +import { HotkeysService, BaseComponent } from 'terminus-core' const INPUT_TIMEOUT = 1000 @@ -36,11 +35,10 @@ const INPUT_TIMEOUT = 1000 ]), ], }) -export class HotkeyInputModalComponent { +export class HotkeyInputModalComponent extends BaseComponent { @Input() value: string[] = [] @Input() timeoutProgress = 0 - private keySubscription: Subscription private lastKeyEvent: number|null = null private keyTimeoutInterval: number|null = null @@ -48,8 +46,9 @@ export class HotkeyInputModalComponent { private modalInstance: NgbActiveModal, public hotkeys: HotkeysService, ) { + super() this.hotkeys.clearCurrentKeystrokes() - this.keySubscription = hotkeys.key.subscribe((event) => { + this.subscribeUntilDestroyed(hotkeys.key, (event) => { this.lastKeyEvent = performance.now() this.value = this.hotkeys.getCurrentKeystrokes() event.preventDefault() @@ -75,10 +74,10 @@ export class HotkeyInputModalComponent { } ngOnDestroy (): void { - this.keySubscription.unsubscribe() this.hotkeys.clearCurrentKeystrokes() this.hotkeys.enable() clearInterval(this.keyTimeoutInterval!) + super.ngOnDestroy() } close (): void { diff --git a/terminus-settings/src/components/settingsTab.component.ts b/terminus-settings/src/components/settingsTab.component.ts index 52de1de3..c5238aeb 100644 --- a/terminus-settings/src/components/settingsTab.component.ts +++ b/terminus-settings/src/components/settingsTab.component.ts @@ -58,7 +58,7 @@ export class SettingsTabComponent extends BaseTabComponent { && config.store.appearance.tabsLocation !== 'top' } - this.configSubscription = config.changed$.subscribe(onConfigChange) + this.configSubscription = this.subscribeUntilDestroyed(config.changed$, onConfigChange) onConfigChange() } diff --git a/terminus-settings/src/components/windowSettingsTab.component.ts b/terminus-settings/src/components/windowSettingsTab.component.ts index 46e6cc41..c009390c 100644 --- a/terminus-settings/src/components/windowSettingsTab.component.ts +++ b/terminus-settings/src/components/windowSettingsTab.component.ts @@ -9,6 +9,7 @@ import { Platform, isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED, + BaseComponent, } from 'terminus-core' @@ -17,7 +18,7 @@ import { selector: 'window-settings-tab', template: require('./windowSettingsTab.component.pug'), }) -export class WindowSettingsTabComponent { +export class WindowSettingsTabComponent extends BaseComponent { screens: any[] Platform = Platform isFluentVibrancySupported = false @@ -29,10 +30,11 @@ export class WindowSettingsTabComponent { public zone: NgZone, @Inject(Theme) public themes: Theme[], ) { + super() this.screens = this.docking.getScreens() this.themes = config.enabledServices(this.themes) - hostApp.displaysChanged$.subscribe(() => { + this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => { this.zone.run(() => this.screens = this.docking.getScreens()) }) diff --git a/terminus-ssh/src/components/sshTab.component.ts b/terminus-ssh/src/components/sshTab.component.ts index 0619da29..1424a4a6 100644 --- a/terminus-ssh/src/components/sshTab.component.ts +++ b/terminus-ssh/src/components/sshTab.component.ts @@ -8,7 +8,6 @@ import { BaseTerminalTabComponent } from 'terminus-terminal' import { SSHService } from '../services/ssh.service' import { SSHConnection, SSHSession } from '../api' import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component' -import { Subscription } from 'rxjs' /** @hidden */ @@ -22,7 +21,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent { connection?: SSHConnection session: SSHSession|null = null private sessionStack: SSHSession[] = [] - private homeEndSubscription: Subscription private recentInputs = '' private reconnectOffered = false private spinner = new Spinner({ @@ -50,7 +48,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent { this.enableDynamicTitle = !this.connection.disableDynamicTitle - this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => { if (!this.hasFocus) { return } @@ -225,11 +223,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent { )).response === 1 } - ngOnDestroy (): void { - this.homeEndSubscription.unsubscribe() - super.ngOnDestroy() - } - private startSpinner () { this.spinner.setSpinnerString(6) this.spinner.start() diff --git a/terminus-terminal/src/api/baseTerminalTab.component.ts b/terminus-terminal/src/api/baseTerminalTab.component.ts index ef318173..66843ea6 100644 --- a/terminus-terminal/src/api/baseTerminalTab.component.ts +++ b/terminus-terminal/src/api/baseTerminalTab.component.ts @@ -4,7 +4,7 @@ import { first } from 'rxjs/operators' import colors from 'ansi-colors' import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core' import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations' -import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent } from 'terminus-core' +import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer } from 'terminus-core' import { BaseSession, SessionsService } from '../services/sessions.service' import { TerminalFrontendService } from '../services/terminalFrontend.service' @@ -95,12 +95,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit protected logger: Logger protected output = new Subject() protected sessionChanged = new Subject() - private sessionCloseSubscription: Subscription - private hotkeysSubscription: Subscription private bellPlayer: HTMLAudioElement - private termContainerSubscriptions: Subscription[] = [] + private termContainerSubscriptions = new SubscriptionContainer() private allFocusModeSubscription: Subscription|null = null - private sessionHandlers: Subscription[] = [] + private sessionHandlers = new SubscriptionContainer() get input$ (): Observable { if (!this.frontend) { @@ -149,7 +147,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.logger = this.log.create('baseTerminalTab') this.setTitle('Terminal') - this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(async hotkey => { + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, async hotkey => { if (!this.hasFocus) { return } @@ -475,7 +473,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit /** @hidden */ ngOnDestroy (): void { + super.ngOnDestroy() + } + + async destroy (): Promise { this.frontend?.detach(this.content.nativeElement) + this.frontend = undefined + this.content.nativeElement.remove() this.detachTermContainerHandlers() this.config.enabledServices(this.decorators).forEach(decorator => { try { @@ -484,14 +488,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.logger.warn('Decorator attach() throws', e) } }) - this.hotkeysSubscription.unsubscribe() - if (this.sessionCloseSubscription) { - this.sessionCloseSubscription.unsubscribe() - } this.output.complete() - } - async destroy (): Promise { super.destroy() if (this.session?.open) { await this.session.destroy() @@ -499,10 +497,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit } protected detachTermContainerHandlers (): void { - for (const subscription of this.termContainerSubscriptions) { - subscription.unsubscribe() - } - this.termContainerSubscriptions = [] + this.termContainerSubscriptions.cancelAll() } protected attachTermContainerHandlers (): void { @@ -518,71 +513,69 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit } } - this.termContainerSubscriptions = [ - this.frontend.title$.subscribe(title => this.zone.run(() => { - if (this.enableDynamicTitle) { - this.setTitle(title) + this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => { + if (this.enableDynamicTitle) { + this.setTitle(title) + } + })) + + this.termContainerSubscriptions.subscribe(this.focused$, () => this.frontend && (this.frontend.enableResizing = true)) + this.termContainerSubscriptions.subscribe(this.blurred$, () => this.frontend && (this.frontend.enableResizing = false)) + + this.termContainerSubscriptions.subscribe(this.frontend.mouseEvent$, async event => { + if (event.type === 'mousedown') { + if (event.which === 2) { + if (this.config.store.terminal.pasteOnMiddleClick) { + this.paste() + } + event.preventDefault() + event.stopPropagation() + return } - })), - - this.focused$.subscribe(() => this.frontend && (this.frontend.enableResizing = true)), - this.blurred$.subscribe(() => this.frontend && (this.frontend.enableResizing = false)), - - this.frontend.mouseEvent$.subscribe(async event => { - if (event.type === 'mousedown') { - if (event.which === 2) { - if (this.config.store.terminal.pasteOnMiddleClick) { - this.paste() - } - event.preventDefault() - event.stopPropagation() - return - } - if (event.which === 3 || event.which === 1 && event.ctrlKey) { - if (this.config.store.terminal.rightClick === 'menu') { - this.hostApp.popupContextMenu(await this.buildContextMenu()) - } else if (this.config.store.terminal.rightClick === 'paste') { - this.paste() - } - event.preventDefault() - event.stopPropagation() - return + if (event.which === 3 || event.which === 1 && event.ctrlKey) { + if (this.config.store.terminal.rightClick === 'menu') { + this.hostApp.popupContextMenu(await this.buildContextMenu()) + } else if (this.config.store.terminal.rightClick === 'paste') { + this.paste() } + event.preventDefault() + event.stopPropagation() + return } - if (event.type === 'mousewheel') { - let wheelDeltaY = 0 + } + if (event.type === 'mousewheel') { + let wheelDeltaY = 0 - if ('wheelDeltaY' in event) { - wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY'] - } else { - wheelDeltaY = (event as MouseWheelEvent)['deltaY'] - } - - if (event.altKey) { - event.preventDefault() - const delta = Math.round(wheelDeltaY / 50) - this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta))) - } + if ('wheelDeltaY' in event) { + wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY'] + } else { + wheelDeltaY = (event as MouseWheelEvent)['deltaY'] } - }), - this.frontend.input$.subscribe(data => { - this.sendInput(data) - }), + if (event.altKey) { + event.preventDefault() + const delta = Math.round(wheelDeltaY / 50) + this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta))) + } + } + }) - this.frontend.resize$.subscribe(({ columns, rows }) => { - this.logger.debug(`Resizing to ${columns}x${rows}`) - this.size = { columns, rows } - this.zone.run(() => { - if (this.session?.open) { - this.session.resize(columns, rows) - } - }) - }), + this.termContainerSubscriptions.subscribe(this.frontend.input$, data => { + this.sendInput(data) + }) - this.hostApp.displayMetricsChanged$.subscribe(maybeConfigure), - this.hostApp.windowMoved$.subscribe(maybeConfigure), - ] + this.termContainerSubscriptions.subscribe(this.frontend.resize$, ({ columns, rows }) => { + this.logger.debug(`Resizing to ${columns}x${rows}`) + this.size = { columns, rows } + this.zone.run(() => { + if (this.session?.open) { + this.session.resize(columns, rows) + } + }) + }) + + this.termContainerSubscriptions.subscribe(this.hostApp.displayMetricsChanged$, maybeConfigure) + this.termContainerSubscriptions.subscribe(this.hostApp.windowMoved$, maybeConfigure) } setSession (session: BaseSession|null, destroyOnSessionClose = false): void { @@ -600,8 +593,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.sessionChanged.next(session) } - protected attachSessionHandler (subscription: Subscription): void { - this.sessionHandlers.push(subscription) + protected attachSessionHandler (observable: Observable, handler: (v: T) => void): void { + this.sessionHandlers.subscribe(observable, handler) } protected attachSessionHandlers (destroyOnSessionClose = false): void { @@ -610,29 +603,26 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit } // this.session.output$.bufferTime(10).subscribe((datas) => { - this.attachSessionHandler(this.session.output$.subscribe(data => { + this.attachSessionHandler(this.session.output$, data => { if (this.enablePassthrough) { this.output.next(data) this.write(data) } - })) + }) if (destroyOnSessionClose) { - this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => { + this.attachSessionHandler(this.session.closed$, () => { this.frontend?.destroy() this.destroy() - })) + }) } - this.attachSessionHandler(this.session.destroyed$.subscribe(() => { + this.attachSessionHandler(this.session.destroyed$, () => { this.setSession(null) - })) + }) } protected detachSessionHandlers (): void { - for (const s of this.sessionHandlers) { - s.unsubscribe() - } - this.sessionHandlers = [] + this.sessionHandlers.cancelAll() } } diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index bc49d571..96a87c51 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -1,5 +1,4 @@ import { Component, Input, Injector } from '@angular/core' -import { Subscription } from 'rxjs' import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' import { SessionOptions } from '../api/interfaces' @@ -14,7 +13,6 @@ import { Session } from '../services/sessions.service' }) export class TerminalTabComponent extends BaseTerminalTabComponent { @Input() sessionOptions: SessionOptions - private homeEndSubscription: Subscription session: Session|null = null // eslint-disable-next-line @typescript-eslint/no-useless-constructor @@ -30,7 +28,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent { const isConPTY = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY - this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { + this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => { if (!this.hasFocus) { return } @@ -106,7 +104,6 @@ export class TerminalTabComponent extends BaseTerminalTabComponent { } ngOnDestroy (): void { - this.homeEndSubscription.unsubscribe() super.ngOnDestroy() this.session?.destroy() } diff --git a/terminus-terminal/src/features/debug.ts b/terminus-terminal/src/features/debug.ts index 5f07414f..ea52545c 100644 --- a/terminus-terminal/src/features/debug.ts +++ b/terminus-terminal/src/features/debug.ts @@ -25,7 +25,7 @@ export class DebugDecorator extends TerminalDecorator { } })) - terminal.content.nativeElement.addEventListener('keyup', e => { + terminal.addEventListenerUntilDestroyed(terminal.content.nativeElement, 'keyup', (e: KeyboardEvent) => { // Ctrl-Shift-Alt-1 if (e.which === 49 && e.ctrlKey && e.shiftKey && e.altKey) { this.doSaveState(terminal) diff --git a/terminus-terminal/src/frontends/xtermFrontend.ts b/terminus-terminal/src/frontends/xtermFrontend.ts index 5b6a8ae2..bd2b7567 100644 --- a/terminus-terminal/src/frontends/xtermFrontend.ts +++ b/terminus-terminal/src/frontends/xtermFrontend.ts @@ -34,7 +34,9 @@ export class XTermFrontend extends Frontend { private fitAddon = new FitAddon() private serializeAddon = new SerializeAddon() private ligaturesAddon?: LigaturesAddon + private webGLAddon?: WebglAddon private opened = false + private resizeObserver?: any constructor () { super() @@ -141,7 +143,8 @@ export class XTermFrontend extends Frontend { await new Promise(resolve => setTimeout(resolve, process.env.XWEB ? 1000 : 0)) if (this.enableWebGL) { - this.xterm.loadAddon(new WebglAddon()) + this.webGLAddon = new WebglAddon() + this.xterm.loadAddon(this.webGLAddon) } this.ready.next() @@ -160,12 +163,19 @@ export class XTermFrontend extends Frontend { host.addEventListener('mouseup', event => this.mouseEvent.next(event)) host.addEventListener('mousewheel', event => this.mouseEvent.next(event as MouseEvent)) - const ro = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler())) - ro.observe(host) + this.resizeObserver = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler())) + this.resizeObserver.observe(host) } detach (_host: HTMLElement): void { window.removeEventListener('resize', this.resizeHandler) + this.resizeObserver?.disconnect() + } + + destroy (): void { + super.destroy() + this.webGLAddon?.dispose() + this.xterm.dispose() } getSelection (): string { diff --git a/webpack.plugin.config.js b/webpack.plugin.config.js index a2f1e5dc..e59ec4c2 100644 --- a/webpack.plugin.config.js +++ b/webpack.plugin.config.js @@ -7,7 +7,7 @@ const bundleAnalyzer = new BundleAnalyzerPlugin({ module.exports = options => { const isDev = !!process.env.TERMINUS_DEV - const devtool = isDev && process.platform === 'win32' ? 'eval-cheap-module-source-map' : 'cheap-module-source-map' + const devtool = process.env.WEBPACK_DEVTOOL ?? (isDev && process.platform === 'win32' ? 'eval-cheap-module-source-map' : 'cheap-module-source-map') const config = { target: 'node', entry: 'src/index.ts',