plugged memory leaks

This commit is contained in:
Eugene Pankov 2021-05-13 16:40:23 +02:00
parent c98fd2042d
commit 5c22e22caa
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
18 changed files with 211 additions and 144 deletions

View File

@ -21,7 +21,7 @@ if (process.platform === 'win32' && !('HOME' in process.env)) {
process.env.HOME = `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` 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') console.warn('Running in debug mode')
} else { } else {
enableProdMode() enableProdMode()

View File

@ -1,3 +1,4 @@
export { BaseComponent, SubscriptionContainer } from '../components/base.component'
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component' export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
export { TabHeaderComponent } from '../components/tabHeader.component' export { TabHeaderComponent } from '../components/tabHeader.component'
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component' export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'

View File

@ -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 <T> (observable: Observable<T>, 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 <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.subscriptionContainer.subscribe(observable, handler)
}
ngOnDestroy (): void {
this.subscriptionContainer.cancelAll()
}
}

View File

@ -1,6 +1,7 @@
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { ViewRef } from '@angular/core' import { ViewRef } from '@angular/core'
import { RecoveryToken } from '../api/tabRecovery' import { RecoveryToken } from '../api/tabRecovery'
import { BaseComponent } from './base.component'
/** /**
* Represents an active "process" inside a tab, * Represents an active "process" inside a tab,
@ -13,7 +14,7 @@ export interface BaseTabProcess {
/** /**
* Abstract base class for custom tab components * Abstract base class for custom tab components
*/ */
export abstract class BaseTabComponent { export abstract class BaseTabComponent extends BaseComponent {
/** /**
* Parent tab (usually a SplitTabComponent) * Parent tab (usually a SplitTabComponent)
*/ */
@ -69,6 +70,7 @@ export abstract class BaseTabComponent {
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint } get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
protected constructor () { protected constructor () {
super()
this.focused$.subscribe(() => { this.focused$.subscribe(() => {
this.hasFocus = true this.hasFocus = true
}) })
@ -158,10 +160,17 @@ export abstract class BaseTabComponent {
this.blurred.complete() this.blurred.complete()
this.titleChange.complete() this.titleChange.complete()
this.progress.complete() this.progress.complete()
this.activity.complete()
this.recoveryStateChangedHint.complete() this.recoveryStateChangedHint.complete()
if (!skipDestroyedEvent) { if (!skipDestroyedEvent) {
this.destroyed.next() this.destroyed.next()
} }
this.destroyed.complete() this.destroyed.complete()
} }
/** @hidden */
ngOnDestroy (): void {
this.destroy()
super.ngOnDestroy()
}
} }

View File

@ -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 { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core'
import { BaseTabComponent, BaseTabProcess } from './baseTab.component' import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery' import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
@ -163,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */ /** @hidden */
private focusedTab: BaseTabComponent|null = null private focusedTab: BaseTabComponent|null = null
private maximizedTab: BaseTabComponent|null = null private maximizedTab: BaseTabComponent|null = null
private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map() private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
private tabAdded = new Subject<BaseTabComponent>() private tabAdded = new Subject<BaseTabComponent>()
@ -210,7 +209,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
}) })
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred())) 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) { if (!this.hasFocus || !this.focusedTab) {
return return
} }
@ -272,7 +271,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */ /** @hidden */
ngOnDestroy (): void { ngOnDestroy (): void {
this.hotkeysSubscription.unsubscribe() this.tabAdded.complete()
this.tabRemoved.complete()
super.ngOnDestroy()
} }
/** @returns Flat list of all sub-tabs */ /** @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<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
this.viewRefs.set(tab, ref) 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.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity()) tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
tab.progress$.subscribe(p => this.setProgress(p)) tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
if (tab.title) { if (tab.title) {
this.setTitle(tab.title) this.setTitle(tab.title)
} }
tab.recoveryStateChangedHint$.subscribe(() => { tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
this.recoveryStateChangedHint.next() this.recoveryStateChangedHint.next()
}) })
tab.destroyed$.subscribe(() => { tab.subscribeUntilDestroyed(tab.destroyed$, () => {
this.removeTab(tab) this.removeTab(tab)
}) })
} }

View File

@ -11,6 +11,7 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service' import { HostAppService, Platform } from '../services/hostApp.service'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { BaseComponent } from './base.component'
/** @hidden */ /** @hidden */
export interface SortableComponentProxy { export interface SortableComponentProxy {
@ -23,7 +24,7 @@ export interface SortableComponentProxy {
template: require('./tabHeader.component.pug'), template: require('./tabHeader.component.pug'),
styles: [require('./tabHeader.component.scss')], styles: [require('./tabHeader.component.scss')],
}) })
export class TabHeaderComponent { export class TabHeaderComponent extends BaseComponent {
@Input() index: number @Input() index: number
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@ -41,7 +42,8 @@ export class TabHeaderComponent {
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy, @Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[], @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 (this.app.activeTab === this.tab) {
if (hotkey === 'rename-tab') { if (hotkey === 'rename-tab') {
this.showRenameTabModal() this.showRenameTabModal()
@ -52,7 +54,7 @@ export class TabHeaderComponent {
} }
ngOnInit () { ngOnInit () {
this.tab.progress$.subscribe(progress => { this.subscribeUntilDestroyed(this.tab.progress$, progress => {
this.zone.run(() => { this.zone.run(() => {
this.progress = progress this.progress = progress
}) })

View File

@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core' import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider' import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence } from './hotkeys.util' import { stringifyKeySequence, EventData } from './hotkeys.util'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service' import { HostAppService } from './hostApp.service'
@ -14,10 +14,6 @@ export interface PartialHotkeyMatch {
const KEY_TIMEOUT = 2000 const KEY_TIMEOUT = 2000
interface EventBufferEntry {
event: KeyboardEvent
time: number
}
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class HotkeysService { export class HotkeysService {
@ -32,7 +28,7 @@ export class HotkeysService {
get hotkey$ (): Observable<string> { return this._hotkey } get hotkey$ (): Observable<string> { return this._hotkey }
private _hotkey = new Subject<string>() private _hotkey = new Subject<string>()
private currentKeystrokes: EventBufferEntry[] = [] private currentKeystrokes: EventData[] = []
private disabledLevel = 0 private disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = [] private hotkeyDescriptions: HotkeyDescription[] = []
@ -73,7 +69,16 @@ export class HotkeysService {
*/ */
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void { pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
(nativeEvent as any).event = name (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[] { getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT) 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 { getCurrentFullyMatchedHotkey (): string|null {

View File

@ -10,15 +10,26 @@ export const altKeyName = {
linux: 'Alt', linux: 'Alt',
}[process.platform] }[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]$/ const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export function stringifyKeySequence (events: KeyboardEvent[]): string[] { export function stringifyKeySequence (events: EventData[]): string[] {
const items: string[] = [] const items: string[] = []
events = events.slice() events = events.slice()
while (events.length > 0) { while (events.length > 0) {
const event = events.shift()! const event = events.shift()!
if ((event as any).event === 'keydown') { if (event.eventName === 'keydown') {
const itemKeys: string[] = [] const itemKeys: string[] = []
if (event.ctrlKey) { if (event.ctrlKey) {
itemKeys.push('Ctrl') itemKeys.push('Ctrl')

View File

@ -6,7 +6,6 @@ import { first } from 'rxjs/operators'
import { BaseTerminalTabComponent } from 'terminus-terminal' import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SerialService } from '../services/serial.service' import { SerialService } from '../services/serial.service'
import { SerialConnection, SerialSession, BAUD_RATES } from '../api' import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
import { Subscription } from 'rxjs'
/** @hidden */ /** @hidden */
@Component({ @Component({
@ -20,7 +19,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
session: SerialSession|null = null session: SerialSession|null = null
serialPort: any serialPort: any
private serialService: SerialService private serialService: SerialService
private homeEndSubscription: Subscription
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor ( constructor (
@ -33,7 +31,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
ngOnInit () { ngOnInit () {
this.logger = this.log.create('terminalTab') this.logger = this.log.create('terminalTab')
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
return return
} }
@ -130,9 +128,4 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
this.serialPort.update({ baudRate: rate }) this.serialPort.update({ baudRate: rate })
this.connection!.baudrate = rate this.connection!.baudrate = rate
} }
ngOnDestroy () {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
}
} }

View File

@ -1,8 +1,7 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { trigger, transition, style, animate } from '@angular/animations' import { trigger, transition, style, animate } from '@angular/animations'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs' import { HotkeysService, BaseComponent } from 'terminus-core'
import { HotkeysService } from 'terminus-core'
const INPUT_TIMEOUT = 1000 const INPUT_TIMEOUT = 1000
@ -36,11 +35,10 @@ const INPUT_TIMEOUT = 1000
]), ]),
], ],
}) })
export class HotkeyInputModalComponent { export class HotkeyInputModalComponent extends BaseComponent {
@Input() value: string[] = [] @Input() value: string[] = []
@Input() timeoutProgress = 0 @Input() timeoutProgress = 0
private keySubscription: Subscription
private lastKeyEvent: number|null = null private lastKeyEvent: number|null = null
private keyTimeoutInterval: number|null = null private keyTimeoutInterval: number|null = null
@ -48,8 +46,9 @@ export class HotkeyInputModalComponent {
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
public hotkeys: HotkeysService, public hotkeys: HotkeysService,
) { ) {
super()
this.hotkeys.clearCurrentKeystrokes() this.hotkeys.clearCurrentKeystrokes()
this.keySubscription = hotkeys.key.subscribe((event) => { this.subscribeUntilDestroyed(hotkeys.key, (event) => {
this.lastKeyEvent = performance.now() this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes() this.value = this.hotkeys.getCurrentKeystrokes()
event.preventDefault() event.preventDefault()
@ -75,10 +74,10 @@ export class HotkeyInputModalComponent {
} }
ngOnDestroy (): void { ngOnDestroy (): void {
this.keySubscription.unsubscribe()
this.hotkeys.clearCurrentKeystrokes() this.hotkeys.clearCurrentKeystrokes()
this.hotkeys.enable() this.hotkeys.enable()
clearInterval(this.keyTimeoutInterval!) clearInterval(this.keyTimeoutInterval!)
super.ngOnDestroy()
} }
close (): void { close (): void {

View File

@ -58,7 +58,7 @@ export class SettingsTabComponent extends BaseTabComponent {
&& config.store.appearance.tabsLocation !== 'top' && config.store.appearance.tabsLocation !== 'top'
} }
this.configSubscription = config.changed$.subscribe(onConfigChange) this.configSubscription = this.subscribeUntilDestroyed(config.changed$, onConfigChange)
onConfigChange() onConfigChange()
} }

View File

@ -9,6 +9,7 @@ import {
Platform, Platform,
isWindowsBuild, isWindowsBuild,
WIN_BUILD_FLUENT_BG_SUPPORTED, WIN_BUILD_FLUENT_BG_SUPPORTED,
BaseComponent,
} from 'terminus-core' } from 'terminus-core'
@ -17,7 +18,7 @@ import {
selector: 'window-settings-tab', selector: 'window-settings-tab',
template: require('./windowSettingsTab.component.pug'), template: require('./windowSettingsTab.component.pug'),
}) })
export class WindowSettingsTabComponent { export class WindowSettingsTabComponent extends BaseComponent {
screens: any[] screens: any[]
Platform = Platform Platform = Platform
isFluentVibrancySupported = false isFluentVibrancySupported = false
@ -29,10 +30,11 @@ export class WindowSettingsTabComponent {
public zone: NgZone, public zone: NgZone,
@Inject(Theme) public themes: Theme[], @Inject(Theme) public themes: Theme[],
) { ) {
super()
this.screens = this.docking.getScreens() this.screens = this.docking.getScreens()
this.themes = config.enabledServices(this.themes) this.themes = config.enabledServices(this.themes)
hostApp.displaysChanged$.subscribe(() => { this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
this.zone.run(() => this.screens = this.docking.getScreens()) this.zone.run(() => this.screens = this.docking.getScreens())
}) })

View File

@ -8,7 +8,6 @@ import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SSHService } from '../services/ssh.service' import { SSHService } from '../services/ssh.service'
import { SSHConnection, SSHSession } from '../api' import { SSHConnection, SSHSession } from '../api'
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component' import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
import { Subscription } from 'rxjs'
/** @hidden */ /** @hidden */
@ -22,7 +21,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
connection?: SSHConnection connection?: SSHConnection
session: SSHSession|null = null session: SSHSession|null = null
private sessionStack: SSHSession[] = [] private sessionStack: SSHSession[] = []
private homeEndSubscription: Subscription
private recentInputs = '' private recentInputs = ''
private reconnectOffered = false private reconnectOffered = false
private spinner = new Spinner({ private spinner = new Spinner({
@ -50,7 +48,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
this.enableDynamicTitle = !this.connection.disableDynamicTitle this.enableDynamicTitle = !this.connection.disableDynamicTitle
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
return return
} }
@ -225,11 +223,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
)).response === 1 )).response === 1
} }
ngOnDestroy (): void {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
}
private startSpinner () { private startSpinner () {
this.spinner.setSpinnerString(6) this.spinner.setSpinnerString(6)
this.spinner.start() this.spinner.start()

View File

@ -4,7 +4,7 @@ import { first } from 'rxjs/operators'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core' import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core'
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations' 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 { BaseSession, SessionsService } from '../services/sessions.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service' import { TerminalFrontendService } from '../services/terminalFrontend.service'
@ -95,12 +95,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected logger: Logger protected logger: Logger
protected output = new Subject<string>() protected output = new Subject<string>()
protected sessionChanged = new Subject<BaseSession|null>() protected sessionChanged = new Subject<BaseSession|null>()
private sessionCloseSubscription: Subscription
private hotkeysSubscription: Subscription
private bellPlayer: HTMLAudioElement private bellPlayer: HTMLAudioElement
private termContainerSubscriptions: Subscription[] = [] private termContainerSubscriptions = new SubscriptionContainer()
private allFocusModeSubscription: Subscription|null = null private allFocusModeSubscription: Subscription|null = null
private sessionHandlers: Subscription[] = [] private sessionHandlers = new SubscriptionContainer()
get input$ (): Observable<Buffer> { get input$ (): Observable<Buffer> {
if (!this.frontend) { if (!this.frontend) {
@ -149,7 +147,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.logger = this.log.create('baseTerminalTab') this.logger = this.log.create('baseTerminalTab')
this.setTitle('Terminal') this.setTitle('Terminal')
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(async hotkey => { this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, async hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
return return
} }
@ -475,7 +473,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
/** @hidden */ /** @hidden */
ngOnDestroy (): void { ngOnDestroy (): void {
super.ngOnDestroy()
}
async destroy (): Promise<void> {
this.frontend?.detach(this.content.nativeElement) this.frontend?.detach(this.content.nativeElement)
this.frontend = undefined
this.content.nativeElement.remove()
this.detachTermContainerHandlers() this.detachTermContainerHandlers()
this.config.enabledServices(this.decorators).forEach(decorator => { this.config.enabledServices(this.decorators).forEach(decorator => {
try { try {
@ -484,14 +488,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.logger.warn('Decorator attach() throws', e) this.logger.warn('Decorator attach() throws', e)
} }
}) })
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
}
this.output.complete() this.output.complete()
}
async destroy (): Promise<void> {
super.destroy() super.destroy()
if (this.session?.open) { if (this.session?.open) {
await this.session.destroy() await this.session.destroy()
@ -499,10 +497,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
protected detachTermContainerHandlers (): void { protected detachTermContainerHandlers (): void {
for (const subscription of this.termContainerSubscriptions) { this.termContainerSubscriptions.cancelAll()
subscription.unsubscribe()
}
this.termContainerSubscriptions = []
} }
protected attachTermContainerHandlers (): void { protected attachTermContainerHandlers (): void {
@ -518,71 +513,69 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
} }
this.termContainerSubscriptions = [ this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => {
this.frontend.title$.subscribe(title => this.zone.run(() => { if (this.enableDynamicTitle) {
if (this.enableDynamicTitle) { this.setTitle(title)
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
} }
})), if (event.which === 3 || event.which === 1 && event.ctrlKey) {
if (this.config.store.terminal.rightClick === 'menu') {
this.focused$.subscribe(() => this.frontend && (this.frontend.enableResizing = true)), this.hostApp.popupContextMenu(await this.buildContextMenu())
this.blurred$.subscribe(() => this.frontend && (this.frontend.enableResizing = false)), } else if (this.config.store.terminal.rightClick === 'paste') {
this.paste()
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
} }
event.preventDefault()
event.stopPropagation()
return
} }
if (event.type === 'mousewheel') { }
let wheelDeltaY = 0 if (event.type === 'mousewheel') {
let wheelDeltaY = 0
if ('wheelDeltaY' in event) { if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY'] wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else { } else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY'] 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)))
}
} }
}),
this.frontend.input$.subscribe(data => { if (event.altKey) {
this.sendInput(data) 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.termContainerSubscriptions.subscribe(this.frontend.input$, data => {
this.logger.debug(`Resizing to ${columns}x${rows}`) this.sendInput(data)
this.size = { columns, rows } })
this.zone.run(() => {
if (this.session?.open) {
this.session.resize(columns, rows)
}
})
}),
this.hostApp.displayMetricsChanged$.subscribe(maybeConfigure), this.termContainerSubscriptions.subscribe(this.frontend.resize$, ({ columns, rows }) => {
this.hostApp.windowMoved$.subscribe(maybeConfigure), 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 { setSession (session: BaseSession|null, destroyOnSessionClose = false): void {
@ -600,8 +593,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.sessionChanged.next(session) this.sessionChanged.next(session)
} }
protected attachSessionHandler (subscription: Subscription): void { protected attachSessionHandler <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.sessionHandlers.push(subscription) this.sessionHandlers.subscribe(observable, handler)
} }
protected attachSessionHandlers (destroyOnSessionClose = false): void { protected attachSessionHandlers (destroyOnSessionClose = false): void {
@ -610,29 +603,26 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
// this.session.output$.bufferTime(10).subscribe((datas) => { // this.session.output$.bufferTime(10).subscribe((datas) => {
this.attachSessionHandler(this.session.output$.subscribe(data => { this.attachSessionHandler(this.session.output$, data => {
if (this.enablePassthrough) { if (this.enablePassthrough) {
this.output.next(data) this.output.next(data)
this.write(data) this.write(data)
} }
})) })
if (destroyOnSessionClose) { if (destroyOnSessionClose) {
this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => { this.attachSessionHandler(this.session.closed$, () => {
this.frontend?.destroy() this.frontend?.destroy()
this.destroy() this.destroy()
})) })
} }
this.attachSessionHandler(this.session.destroyed$.subscribe(() => { this.attachSessionHandler(this.session.destroyed$, () => {
this.setSession(null) this.setSession(null)
})) })
} }
protected detachSessionHandlers (): void { protected detachSessionHandlers (): void {
for (const s of this.sessionHandlers) { this.sessionHandlers.cancelAll()
s.unsubscribe()
}
this.sessionHandlers = []
} }
} }

View File

@ -1,5 +1,4 @@
import { Component, Input, Injector } from '@angular/core' import { Component, Input, Injector } from '@angular/core'
import { Subscription } from 'rxjs'
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core' import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
import { SessionOptions } from '../api/interfaces' import { SessionOptions } from '../api/interfaces'
@ -14,7 +13,6 @@ import { Session } from '../services/sessions.service'
}) })
export class TerminalTabComponent extends BaseTerminalTabComponent { export class TerminalTabComponent extends BaseTerminalTabComponent {
@Input() sessionOptions: SessionOptions @Input() sessionOptions: SessionOptions
private homeEndSubscription: Subscription
session: Session|null = null session: Session|null = null
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // 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 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) { if (!this.hasFocus) {
return return
} }
@ -106,7 +104,6 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
} }
ngOnDestroy (): void { ngOnDestroy (): void {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy() super.ngOnDestroy()
this.session?.destroy() this.session?.destroy()
} }

View File

@ -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 // Ctrl-Shift-Alt-1
if (e.which === 49 && e.ctrlKey && e.shiftKey && e.altKey) { if (e.which === 49 && e.ctrlKey && e.shiftKey && e.altKey) {
this.doSaveState(terminal) this.doSaveState(terminal)

View File

@ -34,7 +34,9 @@ export class XTermFrontend extends Frontend {
private fitAddon = new FitAddon() private fitAddon = new FitAddon()
private serializeAddon = new SerializeAddon() private serializeAddon = new SerializeAddon()
private ligaturesAddon?: LigaturesAddon private ligaturesAddon?: LigaturesAddon
private webGLAddon?: WebglAddon
private opened = false private opened = false
private resizeObserver?: any
constructor () { constructor () {
super() super()
@ -141,7 +143,8 @@ export class XTermFrontend extends Frontend {
await new Promise(resolve => setTimeout(resolve, process.env.XWEB ? 1000 : 0)) await new Promise(resolve => setTimeout(resolve, process.env.XWEB ? 1000 : 0))
if (this.enableWebGL) { if (this.enableWebGL) {
this.xterm.loadAddon(new WebglAddon()) this.webGLAddon = new WebglAddon()
this.xterm.loadAddon(this.webGLAddon)
} }
this.ready.next() this.ready.next()
@ -160,12 +163,19 @@ export class XTermFrontend extends Frontend {
host.addEventListener('mouseup', event => this.mouseEvent.next(event)) host.addEventListener('mouseup', event => this.mouseEvent.next(event))
host.addEventListener('mousewheel', event => this.mouseEvent.next(event as MouseEvent)) host.addEventListener('mousewheel', event => this.mouseEvent.next(event as MouseEvent))
const ro = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler())) this.resizeObserver = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler()))
ro.observe(host) this.resizeObserver.observe(host)
} }
detach (_host: HTMLElement): void { detach (_host: HTMLElement): void {
window.removeEventListener('resize', this.resizeHandler) window.removeEventListener('resize', this.resizeHandler)
this.resizeObserver?.disconnect()
}
destroy (): void {
super.destroy()
this.webGLAddon?.dispose()
this.xterm.dispose()
} }
getSelection (): string { getSelection (): string {

View File

@ -7,7 +7,7 @@ const bundleAnalyzer = new BundleAnalyzerPlugin({
module.exports = options => { module.exports = options => {
const isDev = !!process.env.TERMINUS_DEV 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 = { const config = {
target: 'node', target: 'node',
entry: 'src/index.ts', entry: 'src/index.ts',