tabby/terminus-terminal/src/components/terminalTab.component.ts
Eugene Pankov 98a5a95bec wip
2017-07-26 19:29:47 +02:00

366 lines
12 KiB
TypeScript

import { BehaviorSubject, Subject, Subscription } from 'rxjs'
import 'rxjs/add/operator/bufferTime'
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, ThemesService, HostAppService, HotkeysService, Platform } from 'terminus-core'
import { Session, SessionsService } from '../services/sessions.service'
import { TerminalDecorator, ResizeEvent, SessionOptions } from '../api'
import { hterm, preferenceManager } from '../hterm'
@Component({
selector: 'terminalTab',
template: '<div #content class="content" [style.opacity]="htermVisible ? 1 : 0"></div>',
styles: [require('./terminalTab.component.scss')],
})
export class TerminalTabComponent extends BaseTabComponent {
session: Session
@Input() sessionOptions: SessionOptions
@Input() zoom = 0
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
hterm: any
sessionCloseSubscription: Subscription
hotkeysSubscription: Subscription
bell$ = new Subject()
size: ResizeEvent
resize$ = new Subject<ResizeEvent>()
input$ = new Subject<string>()
output$ = new Subject<string>()
contentUpdated$ = new Subject<void>()
alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject<Event>()
htermVisible = false
private bellPlayer: HTMLAudioElement
private io: any
constructor (
private zone: NgZone,
private app: AppService,
private themes: ThemesService,
private hostApp: HostAppService,
private hotkeys: HotkeysService,
private sessions: SessionsService,
public config: ConfigService,
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
) {
super()
this.decorators = this.decorators || []
this.title = 'Terminal'
this.resize$.first().subscribe(async (resizeEvent) => {
this.session = this.sessions.addSession(
Object.assign({}, this.sessionOptions, resizeEvent)
)
setTimeout(() => {
this.session.resize(resizeEvent.width, resizeEvent.height)
}, 1000)
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => {
// let data = datas.join('')
this.zone.run(() => {
this.output$.next(data)
})
this.write(data)
})
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.app.closeTab(this)
})
this.session.releaseInitialDataBuffer()
})
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) {
return
}
if (hotkey === 'copy') {
this.hterm.copySelectionToClipboard()
}
if (hotkey === 'clear') {
this.clear()
}
if (hotkey === 'zoom-in') {
this.zoomIn()
}
if (hotkey === 'zoom-out') {
this.zoomOut()
}
if (hotkey === 'reset-zoom') {
this.resetZoom()
}
})
this.bellPlayer = document.createElement('audio')
this.bellPlayer.src = require<string>('../bell.ogg')
}
getRecoveryToken (): any {
return {
type: 'app:terminal',
recoveryId: this.sessionOptions.recoveryId,
}
}
ngOnInit () {
this.focused$.subscribe(() => {
this.configure()
setTimeout(() => {
this.hterm.scrollPort_.resize()
this.hterm.scrollPort_.focus()
}, 100)
})
this.hterm = new hterm.hterm.Terminal()
this.decorators.forEach((decorator) => {
decorator.attach(this)
})
this.attachHTermHandlers(this.hterm)
this.hterm.onTerminalReady = () => {
this.htermVisible = true
this.hterm.installKeyboard()
this.hterm.scrollPort_.setCtrlVPaste(true)
this.io = this.hterm.io.push()
this.attachIOHandlers(this.io)
}
this.hterm.decorate(this.content.nativeElement)
this.configure()
setTimeout(() => {
this.output$.subscribe(() => {
this.displayActivity()
})
}, 1000)
this.bell$.subscribe(() => {
if (this.config.store.terminal.bell === 'visual') {
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
setTimeout(() => {
this.configure()
}, 125)
}
if (this.config.store.terminal.bell === 'audible') {
this.bellPlayer.play()
}
// TODO audible
})
}
attachHTermHandlers (hterm: any) {
hterm.setWindowTitle = (title) => {
this.zone.run(() => {
this.title = title
})
}
const _setAlternateMode = hterm.setAlternateMode.bind(hterm)
hterm.setAlternateMode = (state) => {
_setAlternateMode(state)
this.alternateScreenActive$.next(state)
}
hterm.primaryScreen_.syncSelectionCaret = () => null
hterm.alternateScreen_.syncSelectionCaret = () => null
hterm.primaryScreen_.terminal = hterm
hterm.alternateScreen_.terminal = hterm
const _onPaste = hterm.scrollPort_.onPaste_.bind(hterm.scrollPort_)
hterm.scrollPort_.onPaste_ = (event) => {
hterm.scrollPort_.pasteTarget_.value = event.clipboardData.getData('text/plain').trim()
_onPaste()
event.preventDefault()
}
const _resize = hterm.scrollPort_.resize.bind(hterm.scrollPort_)
hterm.scrollPort_.resize = () => {
if (!this.hasFocus) {
return
}
_resize()
}
const _onMouse = hterm.onMouse_.bind(hterm)
hterm.onMouse_ = (event) => {
this.mouseEvent$.next(event)
if (event.type === 'mousewheel') {
if (event.ctrlKey || event.metaKey) {
if (event.wheelDeltaY < 0) {
this.zoomIn()
} else {
this.zoomOut()
}
} else if (event.altKey) {
event.preventDefault()
let delta = Math.round(event.wheelDeltaY / 50)
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
}
}
_onMouse(event)
}
hterm.ringBell = () => {
this.bell$.next()
}
for (let screen of [hterm.primaryScreen_, hterm.alternateScreen_]) {
const _insertString = screen.insertString.bind(screen)
screen.insertString = (data) => {
_insertString(data)
this.contentUpdated$.next()
}
const _deleteChars = screen.deleteChars.bind(screen)
screen.deleteChars = (count) => {
let ret = _deleteChars(count)
this.contentUpdated$.next()
return ret
}
}
}
attachIOHandlers (io: any) {
io.onVTKeystroke = io.sendString = (data) => {
this.sendInput(data)
this.zone.run(() => {
this.input$.next(data)
})
}
io.onTerminalResize = (columns, rows) => {
// console.log(`Resizing to ${columns}x${rows}`)
this.zone.run(() => {
this.size = { width: columns, height: rows }
if (this.session) {
this.session.resize(columns, rows)
}
this.resize$.next(this.size)
})
}
}
sendInput (data: string) {
this.session.write(data)
}
write (data: string) {
this.io.writeUTF8(data)
}
clear () {
this.hterm.wipeContents()
this.hterm.onVTKeystroke('\f')
}
async configure (): Promise<void> {
let config = this.config.store
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
this.setFontSize()
preferenceManager.set('enable-bold', true)
// preferenceManager.set('audible-bell-sound', '')
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
preferenceManager.set('enable-clipboard-notice', false)
preferenceManager.set('receive-encoding', 'raw')
preferenceManager.set('send-encoding', 'raw')
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
preferenceManager.set('scrollbar-visible', this.hostApp.platform === Platform.macOS)
preferenceManager.set('copy-on-select', false)
if (config.terminal.colorScheme.foreground) {
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
}
if (config.terminal.background === 'colorScheme') {
if (config.terminal.colorScheme.background) {
this.backgroundColor = config.terminal.colorScheme.background
preferenceManager.set('background-color', config.terminal.colorScheme.background)
}
} else {
this.backgroundColor = null
// hterm can't parse "transparent"
preferenceManager.set('background-color', this.themes.findCurrentTheme().terminalBackground)
}
if (config.terminal.colorScheme.colors) {
preferenceManager.set('color-palette-overrides', config.terminal.colorScheme.colors)
}
if (config.terminal.colorScheme.cursor) {
preferenceManager.set('cursor-color', config.terminal.colorScheme.cursor)
}
let css = require('../hterm.userCSS.scss')
if (!config.terminal.ligatures) {
css += `
* {
font-feature-settings: "liga" 0;
font-variant-ligatures: none;
}
`
} else {
css += `
* {
font-feature-settings: "liga" 1;
font-variant-ligatures: initial;
}
`
}
css += config.appearance.css
this.hterm.setCSS(css)
this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
this.hterm.defaultCursorShape = {
block: hterm.hterm.Terminal.cursorShape.BLOCK,
underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
beam: hterm.hterm.Terminal.cursorShape.BEAM,
}[config.terminal.cursor]
this.hterm.applyCursorShape()
}
zoomIn () {
this.zoom++
this.setFontSize()
}
zoomOut () {
this.zoom--
this.setFontSize()
}
resetZoom () {
this.zoom = 0
this.setFontSize()
}
ngOnDestroy () {
this.decorators.forEach(decorator => {
decorator.detach(this)
})
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
}
this.resize$.complete()
this.input$.complete()
this.output$.complete()
this.contentUpdated$.complete()
this.alternateScreenActive$.complete()
this.mouseEvent$.complete()
this.bell$.complete()
}
async destroy () {
super.destroy()
if (this.session && this.session.open) {
await this.session.destroy()
}
}
async canClose (): Promise<boolean> {
if (this.hostApp.platform === Platform.Windows) {
return true
}
let children = await this.session.getChildProcesses()
if (children.length === 0) {
return true
}
return confirm(`"${children[0].command}" is still running. Close?`)
}
private setFontSize () {
preferenceManager.set('font-size', this.config.store.terminal.fontSize * Math.pow(1.1, this.zoom))
}
}