mirror of
https://github.com/Eugeny/tabby.git
synced 2025-01-30 14:20:18 +08:00
plugged memory leaks
This commit is contained in:
parent
c98fd2042d
commit
5c22e22caa
@ -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()
|
||||
|
@ -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'
|
||||
|
54
terminus-core/src/components/base.component.ts
Normal file
54
terminus-core/src/components/base.component.ts
Normal 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()
|
||||
}
|
||||
}
|
@ -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<void> { 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()
|
||||
}
|
||||
}
|
||||
|
@ -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<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||
|
||||
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.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<any> // 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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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<string> { return this._hotkey }
|
||||
|
||||
private _hotkey = new Subject<string>()
|
||||
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 {
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
})
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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<string>()
|
||||
protected sessionChanged = new Subject<BaseSession|null>()
|
||||
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<Buffer> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 <T> (observable: Observable<T>, 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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user