mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-26 03:51:49 +03:00
split common terminal behaviour into BaseTerminalTab
This commit is contained in:
parent
bcb6963c35
commit
caacc01aea
@ -1,10 +1,10 @@
|
|||||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
|
||||||
|
|
||||||
export abstract class TerminalDecorator {
|
export abstract class TerminalDecorator {
|
||||||
// tslint:disable-next-line no-empty
|
// tslint:disable-next-line no-empty
|
||||||
attach (_terminal: TerminalTabComponent): void { }
|
attach (_terminal: BaseTerminalTabComponent): void { }
|
||||||
// tslint:disable-next-line no-empty
|
// tslint:disable-next-line no-empty
|
||||||
detach (_terminal: TerminalTabComponent): void { }
|
detach (_terminal: BaseTerminalTabComponent): void { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResizeEvent {
|
export interface ResizeEvent {
|
||||||
@ -44,7 +44,7 @@ export abstract class TerminalColorSchemeProvider {
|
|||||||
export abstract class TerminalContextMenuItemProvider {
|
export abstract class TerminalContextMenuItemProvider {
|
||||||
weight: number
|
weight: number
|
||||||
|
|
||||||
abstract async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
abstract async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IShell {
|
export interface IShell {
|
||||||
|
336
terminus-terminal/src/components/baseTerminalTab.component.ts
Normal file
336
terminus-terminal/src/components/baseTerminalTab.component.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import { Observable, Subject, Subscription } from 'rxjs'
|
||||||
|
import { first } from 'rxjs/operators'
|
||||||
|
import { ToastrService } from 'ngx-toastr'
|
||||||
|
import { NgZone, OnInit, OnDestroy, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||||
|
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
|
||||||
|
|
||||||
|
import { Session, SessionsService } from '../services/sessions.service'
|
||||||
|
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||||
|
|
||||||
|
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
|
||||||
|
import { Frontend } from '../frontends/frontend'
|
||||||
|
|
||||||
|
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||||
|
static template = `
|
||||||
|
<div
|
||||||
|
#content
|
||||||
|
class="content"
|
||||||
|
[style.opacity]="htermVisible ? 1 : 0"
|
||||||
|
></div>
|
||||||
|
`
|
||||||
|
static styles = [require('./terminalTab.component.scss')]
|
||||||
|
|
||||||
|
session: Session
|
||||||
|
@Input() zoom = 0
|
||||||
|
@ViewChild('content') content
|
||||||
|
@HostBinding('style.background-color') backgroundColor: string
|
||||||
|
frontend: Frontend
|
||||||
|
sessionCloseSubscription: Subscription
|
||||||
|
hotkeysSubscription: Subscription
|
||||||
|
htermVisible = false
|
||||||
|
frontendReady = new Subject<void>()
|
||||||
|
size: ResizeEvent
|
||||||
|
protected logger: Logger
|
||||||
|
protected output = new Subject<string>()
|
||||||
|
private bellPlayer: HTMLAudioElement
|
||||||
|
private termContainerSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
|
get input$ (): Observable<string> { return this.frontend.input$ }
|
||||||
|
get output$ (): Observable<string> { return this.output }
|
||||||
|
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
||||||
|
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
||||||
|
get frontendReady$ (): Observable<void> { return this.frontendReady }
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public config: ConfigService,
|
||||||
|
protected zone: NgZone,
|
||||||
|
protected app: AppService,
|
||||||
|
protected hostApp: HostAppService,
|
||||||
|
protected hotkeys: HotkeysService,
|
||||||
|
protected sessions: SessionsService,
|
||||||
|
protected electron: ElectronService,
|
||||||
|
protected terminalContainersService: TerminalFrontendService,
|
||||||
|
protected toastr: ToastrService,
|
||||||
|
protected log: LogService,
|
||||||
|
@Optional() @Inject(TerminalDecorator) protected decorators: TerminalDecorator[],
|
||||||
|
@Optional() @Inject(TerminalContextMenuItemProvider) protected contextMenuProviders: TerminalContextMenuItemProvider[],
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.logger = log.create('baseTerminalTab')
|
||||||
|
this.decorators = this.decorators || []
|
||||||
|
this.setTitle('Terminal')
|
||||||
|
|
||||||
|
this.session = new Session(this.config)
|
||||||
|
|
||||||
|
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||||
|
if (!this.hasFocus) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (hotkey) {
|
||||||
|
case 'ctrl-c':
|
||||||
|
if (this.frontend.getSelection()) {
|
||||||
|
this.frontend.copySelection()
|
||||||
|
this.frontend.clearSelection()
|
||||||
|
this.toastr.info('Copied')
|
||||||
|
} else {
|
||||||
|
this.sendInput('\x03')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'copy':
|
||||||
|
this.frontend.copySelection()
|
||||||
|
this.toastr.info('Copied')
|
||||||
|
break
|
||||||
|
case 'paste':
|
||||||
|
this.paste()
|
||||||
|
break
|
||||||
|
case 'clear':
|
||||||
|
this.frontend.clear()
|
||||||
|
break
|
||||||
|
case 'zoom-in':
|
||||||
|
this.zoomIn()
|
||||||
|
break
|
||||||
|
case 'zoom-out':
|
||||||
|
this.zoomOut()
|
||||||
|
break
|
||||||
|
case 'reset-zoom':
|
||||||
|
this.resetZoom()
|
||||||
|
break
|
||||||
|
case 'home':
|
||||||
|
this.sendInput('\x1bOH')
|
||||||
|
break
|
||||||
|
case 'end':
|
||||||
|
this.sendInput('\x1bOF')
|
||||||
|
break
|
||||||
|
case 'previous-word':
|
||||||
|
this.sendInput('\x1bb')
|
||||||
|
break
|
||||||
|
case 'next-word':
|
||||||
|
this.sendInput('\x1bf')
|
||||||
|
break
|
||||||
|
case 'delete-previous-word':
|
||||||
|
this.sendInput('\x1b\x7f')
|
||||||
|
break
|
||||||
|
case 'delete-next-word':
|
||||||
|
this.sendInput('\x1bd')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.bellPlayer = document.createElement('audio')
|
||||||
|
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||||
|
|
||||||
|
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.focused$.subscribe(() => {
|
||||||
|
this.configure()
|
||||||
|
this.frontend.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.frontend = this.terminalContainersService.getFrontend(this.session)
|
||||||
|
|
||||||
|
this.frontend.ready$.subscribe(() => {
|
||||||
|
this.htermVisible = true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
|
||||||
|
this.size = { columns, rows }
|
||||||
|
this.frontendReady.next()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.session.resize(columns, rows)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
this.session.releaseInitialDataBuffer()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.frontend.configure(this.config.store)
|
||||||
|
this.frontend.attach(this.content.nativeElement)
|
||||||
|
this.attachTermContainerHandlers()
|
||||||
|
|
||||||
|
this.configure()
|
||||||
|
|
||||||
|
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||||
|
decorator.attach(this)
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.output.subscribe(() => {
|
||||||
|
this.displayActivity()
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
this.frontend.bell$.subscribe(() => {
|
||||||
|
if (this.config.store.terminal.bell === 'visual') {
|
||||||
|
this.frontend.visualBell()
|
||||||
|
}
|
||||||
|
if (this.config.store.terminal.bell === 'audible') {
|
||||||
|
this.bellPlayer.play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.frontend.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||||
|
let items: Electron.MenuItemConstructorOptions[] = []
|
||||||
|
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
|
||||||
|
items = items.concat(section)
|
||||||
|
items.push({ type: 'separator' })
|
||||||
|
}
|
||||||
|
items.splice(items.length - 1, 1)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
detachTermContainerHandlers () {
|
||||||
|
for (let subscription of this.termContainerSubscriptions) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
this.termContainerSubscriptions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
attachTermContainerHandlers () {
|
||||||
|
this.detachTermContainerHandlers()
|
||||||
|
this.termContainerSubscriptions = [
|
||||||
|
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
|
||||||
|
|
||||||
|
this.focused$.subscribe(() => this.frontend.enableResizing = true),
|
||||||
|
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
|
||||||
|
|
||||||
|
this.frontend.mouseEvent$.subscribe(async event => {
|
||||||
|
if (event.type === 'mousedown') {
|
||||||
|
if (event.which === 3) {
|
||||||
|
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 ('wheelDeltaY' in event) {
|
||||||
|
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
||||||
|
} else {
|
||||||
|
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
||||||
|
}
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
|
||||||
|
if (wheelDeltaY > 0) {
|
||||||
|
this.zoomIn()
|
||||||
|
} else {
|
||||||
|
this.zoomOut()
|
||||||
|
}
|
||||||
|
} else if (event.altKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
let delta = Math.round(wheelDeltaY / 50)
|
||||||
|
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.frontend.input$.subscribe(data => {
|
||||||
|
this.sendInput(data)
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.frontend.resize$.subscribe(({ columns, rows }) => {
|
||||||
|
this.logger.info(`Resizing to ${columns}x${rows}`)
|
||||||
|
this.size = { columns, rows }
|
||||||
|
this.zone.run(() => {
|
||||||
|
if (this.session.open) {
|
||||||
|
this.session.resize(columns, rows)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.hostApp.windowMoved$.subscribe(() => setTimeout(() => {
|
||||||
|
this.configure()
|
||||||
|
}, 250)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInput (data: string) {
|
||||||
|
this.session.write(data)
|
||||||
|
if (this.config.store.terminal.scrollOnInput) {
|
||||||
|
this.frontend.scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write (data: string) {
|
||||||
|
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
||||||
|
if (percentageMatch) {
|
||||||
|
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||||
|
if (percentage > 0 && percentage <= 100) {
|
||||||
|
this.setProgress(percentage)
|
||||||
|
this.logger.debug('Detected progress:', percentage)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setProgress(null)
|
||||||
|
}
|
||||||
|
this.frontend.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
paste () {
|
||||||
|
let data = this.electron.clipboard.readText()
|
||||||
|
if (this.config.store.terminal.bracketedPaste) {
|
||||||
|
data = '\x1b[200~' + data + '\x1b[201~'
|
||||||
|
}
|
||||||
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
|
data = data.replace(/\r\n/g, '\r')
|
||||||
|
} else {
|
||||||
|
data = data.replace(/\n/g, '\r')
|
||||||
|
}
|
||||||
|
this.sendInput(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
configure (): void {
|
||||||
|
this.frontend.configure(this.config.store)
|
||||||
|
|
||||||
|
if (this.config.store.terminal.background === 'colorScheme') {
|
||||||
|
if (this.config.store.terminal.colorScheme.background) {
|
||||||
|
this.backgroundColor = this.config.store.terminal.colorScheme.background
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.backgroundColor = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomIn () {
|
||||||
|
this.zoom++
|
||||||
|
this.frontend.setZoom(this.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomOut () {
|
||||||
|
this.zoom--
|
||||||
|
this.frontend.setZoom(this.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetZoom () {
|
||||||
|
this.zoom = 0
|
||||||
|
this.frontend.setZoom(this.zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
this.frontend.detach(this.content.nativeElement)
|
||||||
|
this.detachTermContainerHandlers()
|
||||||
|
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||||
|
decorator.detach(this)
|
||||||
|
})
|
||||||
|
this.hotkeysSubscription.unsubscribe()
|
||||||
|
if (this.sessionCloseSubscription) {
|
||||||
|
this.sessionCloseSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
this.output.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy () {
|
||||||
|
super.destroy()
|
||||||
|
if (this.session && this.session.open) {
|
||||||
|
await this.session.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,121 +1,25 @@
|
|||||||
import { Observable, Subject, Subscription } from 'rxjs'
|
import { Component, Input } from '@angular/core'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { ToastrService } from 'ngx-toastr'
|
import { BaseTabProcess } from 'terminus-core'
|
||||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||||
import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
import { SessionOptions } from '../api'
|
||||||
|
|
||||||
import { Session, SessionsService } from '../services/sessions.service'
|
|
||||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
|
||||||
|
|
||||||
import { TerminalDecorator, ResizeEvent, SessionOptions, TerminalContextMenuItemProvider } from '../api'
|
|
||||||
import { Frontend } from '../frontends/frontend'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'terminalTab',
|
selector: 'terminalTab',
|
||||||
template: `
|
template: BaseTerminalTabComponent.template,
|
||||||
<div
|
styles: BaseTerminalTabComponent.styles,
|
||||||
#content
|
|
||||||
class="content"
|
|
||||||
[style.opacity]="htermVisible ? 1 : 0"
|
|
||||||
></div>
|
|
||||||
`,
|
|
||||||
styles: [require('./terminalTab.component.scss')],
|
|
||||||
})
|
})
|
||||||
export class TerminalTabComponent extends BaseTabComponent {
|
export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||||
session: Session
|
|
||||||
@Input() sessionOptions: SessionOptions
|
@Input() sessionOptions: SessionOptions
|
||||||
@Input() zoom = 0
|
|
||||||
@ViewChild('content') content
|
|
||||||
@HostBinding('style.background-color') backgroundColor: string
|
|
||||||
frontend: Frontend
|
|
||||||
sessionCloseSubscription: Subscription
|
|
||||||
hotkeysSubscription: Subscription
|
|
||||||
htermVisible = false
|
|
||||||
private output = new Subject<string>()
|
|
||||||
private bellPlayer: HTMLAudioElement
|
|
||||||
private termContainerSubscriptions: Subscription[] = []
|
|
||||||
|
|
||||||
get input$ (): Observable<string> { return this.frontend.input$ }
|
ngOnInit () {
|
||||||
get output$ (): Observable<string> { return this.output }
|
this.logger = this.log.create('terminalTab')
|
||||||
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
|
||||||
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
|
||||||
|
|
||||||
constructor (
|
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||||
private zone: NgZone,
|
this.initializeSession(this.size.columns, this.size.rows)
|
||||||
private app: AppService,
|
|
||||||
private hostApp: HostAppService,
|
|
||||||
private hotkeys: HotkeysService,
|
|
||||||
private sessions: SessionsService,
|
|
||||||
private electron: ElectronService,
|
|
||||||
private terminalContainersService: TerminalFrontendService,
|
|
||||||
public config: ConfigService,
|
|
||||||
private toastr: ToastrService,
|
|
||||||
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
|
|
||||||
@Optional() @Inject(TerminalContextMenuItemProvider) private contextMenuProviders: TerminalContextMenuItemProvider[],
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.decorators = this.decorators || []
|
|
||||||
this.setTitle('Terminal')
|
|
||||||
|
|
||||||
this.session = new Session(this.config)
|
|
||||||
|
|
||||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
|
||||||
if (!this.hasFocus) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (hotkey) {
|
|
||||||
case 'ctrl-c':
|
|
||||||
if (this.frontend.getSelection()) {
|
|
||||||
this.frontend.copySelection()
|
|
||||||
this.frontend.clearSelection()
|
|
||||||
this.toastr.info('Copied')
|
|
||||||
} else {
|
|
||||||
this.sendInput('\x03')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'copy':
|
|
||||||
this.frontend.copySelection()
|
|
||||||
this.toastr.info('Copied')
|
|
||||||
break
|
|
||||||
case 'paste':
|
|
||||||
this.paste()
|
|
||||||
break
|
|
||||||
case 'clear':
|
|
||||||
this.frontend.clear()
|
|
||||||
break
|
|
||||||
case 'zoom-in':
|
|
||||||
this.zoomIn()
|
|
||||||
break
|
|
||||||
case 'zoom-out':
|
|
||||||
this.zoomOut()
|
|
||||||
break
|
|
||||||
case 'reset-zoom':
|
|
||||||
this.resetZoom()
|
|
||||||
break
|
|
||||||
case 'home':
|
|
||||||
this.sendInput('\x1bOH')
|
|
||||||
break
|
|
||||||
case 'end':
|
|
||||||
this.sendInput('\x1bOF')
|
|
||||||
break
|
|
||||||
case 'previous-word':
|
|
||||||
this.sendInput('\x1bb')
|
|
||||||
break
|
|
||||||
case 'next-word':
|
|
||||||
this.sendInput('\x1bf')
|
|
||||||
break
|
|
||||||
case 'delete-previous-word':
|
|
||||||
this.sendInput('\x1b\x7f')
|
|
||||||
break
|
|
||||||
case 'delete-next-word':
|
|
||||||
this.sendInput('\x1bd')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.bellPlayer = document.createElement('audio')
|
|
||||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
|
||||||
|
|
||||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
super.ngOnInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeSession (columns: number, rows: number) {
|
initializeSession (columns: number, rows: number) {
|
||||||
@ -152,199 +56,6 @@ export class TerminalTabComponent extends BaseTabComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.focused$.subscribe(() => {
|
|
||||||
this.configure()
|
|
||||||
this.frontend.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.frontend = this.terminalContainersService.getFrontend(this.session)
|
|
||||||
|
|
||||||
this.frontend.ready$.subscribe(() => {
|
|
||||||
this.htermVisible = true
|
|
||||||
})
|
|
||||||
|
|
||||||
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
|
|
||||||
if (!this.session.open) {
|
|
||||||
this.initializeSession(columns, rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.session.resize(columns, rows)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
this.session.releaseInitialDataBuffer()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.frontend.configure(this.config.store)
|
|
||||||
this.frontend.attach(this.content.nativeElement)
|
|
||||||
this.attachTermContainerHandlers()
|
|
||||||
|
|
||||||
this.configure()
|
|
||||||
|
|
||||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
|
||||||
decorator.attach(this)
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.output.subscribe(() => {
|
|
||||||
this.displayActivity()
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
this.frontend.bell$.subscribe(() => {
|
|
||||||
if (this.config.store.terminal.bell === 'visual') {
|
|
||||||
this.frontend.visualBell()
|
|
||||||
}
|
|
||||||
if (this.config.store.terminal.bell === 'audible') {
|
|
||||||
this.bellPlayer.play()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.frontend.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
|
||||||
let items: Electron.MenuItemConstructorOptions[] = []
|
|
||||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
|
|
||||||
items = items.concat(section)
|
|
||||||
items.push({ type: 'separator' })
|
|
||||||
}
|
|
||||||
items.splice(items.length - 1, 1)
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
detachTermContainerHandlers () {
|
|
||||||
for (let subscription of this.termContainerSubscriptions) {
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
this.termContainerSubscriptions = []
|
|
||||||
}
|
|
||||||
|
|
||||||
attachTermContainerHandlers () {
|
|
||||||
this.detachTermContainerHandlers()
|
|
||||||
this.termContainerSubscriptions = [
|
|
||||||
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
|
|
||||||
|
|
||||||
this.focused$.subscribe(() => this.frontend.enableResizing = true),
|
|
||||||
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
|
|
||||||
|
|
||||||
this.frontend.mouseEvent$.subscribe(async event => {
|
|
||||||
if (event.type === 'mousedown') {
|
|
||||||
if (event.which === 3) {
|
|
||||||
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 ('wheelDeltaY' in event) {
|
|
||||||
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
|
||||||
} else {
|
|
||||||
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
|
||||||
}
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
|
|
||||||
if (wheelDeltaY > 0) {
|
|
||||||
this.zoomIn()
|
|
||||||
} else {
|
|
||||||
this.zoomOut()
|
|
||||||
}
|
|
||||||
} else if (event.altKey) {
|
|
||||||
event.preventDefault()
|
|
||||||
let delta = Math.round(wheelDeltaY / 50)
|
|
||||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.frontend.input$.subscribe(data => {
|
|
||||||
this.sendInput(data)
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.frontend.resize$.subscribe(({ columns, rows }) => {
|
|
||||||
console.log(`Resizing to ${columns}x${rows}`)
|
|
||||||
this.zone.run(() => {
|
|
||||||
if (this.session.open) {
|
|
||||||
this.session.resize(columns, rows)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.hostApp.windowMoved$.subscribe(() => setTimeout(() => {
|
|
||||||
this.configure()
|
|
||||||
}, 250)),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
sendInput (data: string) {
|
|
||||||
this.session.write(data)
|
|
||||||
if (this.config.store.terminal.scrollOnInput) {
|
|
||||||
this.frontend.scrollToBottom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write (data: string) {
|
|
||||||
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
|
||||||
if (percentageMatch) {
|
|
||||||
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
|
||||||
if (percentage > 0 && percentage <= 100) {
|
|
||||||
this.setProgress(percentage)
|
|
||||||
console.log('Detected progress:', percentage)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setProgress(null)
|
|
||||||
}
|
|
||||||
this.frontend.write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
paste () {
|
|
||||||
let data = this.electron.clipboard.readText()
|
|
||||||
if (this.config.store.terminal.bracketedPaste) {
|
|
||||||
data = '\x1b[200~' + data + '\x1b[201~'
|
|
||||||
}
|
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
|
||||||
data = data.replace(/\r\n/g, '\r')
|
|
||||||
} else {
|
|
||||||
data = data.replace(/\n/g, '\r')
|
|
||||||
}
|
|
||||||
this.sendInput(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
configure (): void {
|
|
||||||
this.frontend.configure(this.config.store)
|
|
||||||
|
|
||||||
if (this.config.store.terminal.background === 'colorScheme') {
|
|
||||||
if (this.config.store.terminal.colorScheme.background) {
|
|
||||||
this.backgroundColor = this.config.store.terminal.colorScheme.background
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.backgroundColor = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomIn () {
|
|
||||||
this.zoom++
|
|
||||||
this.frontend.setZoom(this.zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
zoomOut () {
|
|
||||||
this.zoom--
|
|
||||||
this.frontend.setZoom(this.zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetZoom () {
|
|
||||||
this.zoom = 0
|
|
||||||
this.frontend.setZoom(this.zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||||
let children = await this.session.getChildProcesses()
|
let children = await this.session.getChildProcesses()
|
||||||
if (!children.length) {
|
if (!children.length) {
|
||||||
@ -355,26 +66,6 @@ export class TerminalTabComponent extends BaseTabComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
|
||||||
this.frontend.detach(this.content.nativeElement)
|
|
||||||
this.detachTermContainerHandlers()
|
|
||||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
|
||||||
decorator.detach(this)
|
|
||||||
})
|
|
||||||
this.hotkeysSubscription.unsubscribe()
|
|
||||||
if (this.sessionCloseSubscription) {
|
|
||||||
this.sessionCloseSubscription.unsubscribe()
|
|
||||||
}
|
|
||||||
this.output.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy () {
|
|
||||||
super.destroy()
|
|
||||||
if (this.session && this.session.open) {
|
|
||||||
await this.session.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async canClose (): Promise<boolean> {
|
async canClose (): Promise<boolean> {
|
||||||
let children = await this.session.getChildProcesses()
|
let children = await this.session.getChildProcesses()
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
|
@ -16,6 +16,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
|
|||||||
import { ColorPickerComponent } from './components/colorPicker.component'
|
import { ColorPickerComponent } from './components/colorPicker.component'
|
||||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||||
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
|
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
|
||||||
|
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
|
||||||
|
|
||||||
import { BaseSession } from './services/sessions.service'
|
import { BaseSession } from './services/sessions.service'
|
||||||
import { TerminalFrontendService } from './services/terminalFrontend.service'
|
import { TerminalFrontendService } from './services/terminalFrontend.service'
|
||||||
@ -203,5 +204,5 @@ export default class TerminalModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService }
|
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService, BaseTerminalTabComponent }
|
||||||
export * from './api'
|
export * from './api'
|
||||||
|
Loading…
Reference in New Issue
Block a user