diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index 90e5b051..5edb4ea6 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -12,7 +12,9 @@ export interface SSHConnection { export class SSHSession extends BaseSession { constructor (private shell: any) { super() + } + start () { this.open = true this.shell.on('data', data => { diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index 445ec471..3185487a 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -1,4 +1,4 @@ -import { Observable, BehaviorSubject, Subject, Subscription } from 'rxjs' +import { Subject, Subscription } from 'rxjs' import { first } from 'rxjs/operators' import { ToastrService } from 'ngx-toastr' import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core' @@ -7,9 +7,11 @@ import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppSe import { IShell } from '../api' import { Session, SessionsService } from '../services/sessions.service' import { TerminalService } from '../services/terminal.service' +import { TerminalContainersService } from '../services/terminalContainers.service' import { TerminalDecorator, ResizeEvent, SessionOptions } from '../api' -import { hterm, preferenceManager } from '../hterm' +import { TermContainer } from '../terminalContainers/termContainer' +import { hterm } from '../hterm' @Component({ selector: 'terminalTab', @@ -28,24 +30,16 @@ export class TerminalTabComponent extends BaseTabComponent { @Input() zoom = 0 @ViewChild('content') content @HostBinding('style.background-color') backgroundColor: string - hterm: any + termContainer: TermContainer sessionCloseSubscription: Subscription hotkeysSubscription: Subscription - bell$ = new Subject() size: ResizeEvent - resize$: Observable - input$ = new Subject() output$ = new Subject() - contentUpdated$: Observable - alternateScreenActive$ = new BehaviorSubject(false) - mouseEvent$ = new Subject() htermVisible = false shell: IShell - private resize_ = new Subject() - private contentUpdated_ = new Subject() private bellPlayer: HTMLAudioElement - private io: any private contextMenu: any + private termContainerSubscriptions: Subscription[] = [] constructor ( private zone: NgZone, @@ -55,60 +49,40 @@ export class TerminalTabComponent extends BaseTabComponent { private sessions: SessionsService, private electron: ElectronService, private terminalService: TerminalService, + private terminalContainersService: TerminalContainersService, public config: ConfigService, private toastr: ToastrService, @Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[], ) { super() - this.resize$ = this.resize_.asObservable() this.decorators = this.decorators || [] this.setTitle('Terminal') - this.resize$.pipe(first()).subscribe(async resizeEvent => { - if (!this.session) { - this.session = this.sessions.addSession( - Object.assign({}, this.sessionOptions, resizeEvent) - ) - } - setTimeout(() => { - this.session.resize(resizeEvent.width, resizeEvent.height) - }, 1000) + this.session = new Session() - // this.session.output$.bufferTime(10).subscribe((datas) => { - this.session.output$.subscribe(data => { - 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 } switch (hotkey) { case 'ctrl-c': - if (this.hterm.getSelectionText()) { - this.hterm.copySelectionToClipboard() - this.hterm.getDocument().getSelection().removeAllRanges() + if (this.termContainer.getSelection()) { + this.termContainer.copySelection() + this.termContainer.clearSelection() + this.toastr.info('Copied') } else { this.sendInput('\x03') } break case 'copy': - this.hterm.copySelectionToClipboard() + this.termContainer.copySelection() + this.toastr.info('Copied') break case 'paste': this.paste() break case 'clear': - this.clear() + this.termContainer.clear() break case 'zoom-in': this.zoomIn() @@ -143,6 +117,29 @@ export class TerminalTabComponent extends BaseTabComponent { this.bellPlayer.src = require('../bell.ogg') } + initializeSession (columns: number, rows: number) { + this.sessions.addSession( + this.session, + Object.assign({}, this.sessionOptions, { + width: columns, + height: rows, + }) + ) + + // this.session.output$.bufferTime(10).subscribe((datas) => { + this.session.output$.subscribe(data => { + this.zone.run(() => { + this.output$.next(data) + this.write(data) + }) + }) + + this.sessionCloseSubscription = this.session.closed$.subscribe(() => { + this.termContainer.destroy() + this.app.closeTab(this) + }) + } + getRecoveryToken (): any { return { type: 'app:terminal', @@ -153,46 +150,49 @@ export class TerminalTabComponent extends BaseTabComponent { ngOnInit () { this.focused$.subscribe(() => { this.configure() - setTimeout(() => { - this.hterm.scrollPort_.resize() - this.hterm.scrollPort_.focus() - }, 100) + this.termContainer.focus() }) - this.hterm = new hterm.hterm.Terminal() + this.termContainer = this.terminalContainersService.getContainer(this.session) + + this.termContainer.ready$.subscribe(() => { + this.htermVisible = true + }) + + this.termContainer.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.termContainer.attach(this.content.nativeElement) + this.attachTermContainerHandlers() + + this.configure() + this.config.enabledServices(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(() => { + this.termContainer.bell$.subscribe(() => { if (this.config.store.terminal.bell === 'visual') { - preferenceManager.set('background-color', 'rgba(128,128,128,.25)') - setTimeout(() => { - this.configure() - }, 125) + this.termContainer.visualBell() } if (this.config.store.terminal.bell === 'audible') { this.bellPlayer.play() } - // TODO audible }) this.contextMenu = this.electron.remote.Menu.buildFromTemplate([ @@ -209,7 +209,8 @@ export class TerminalTabComponent extends BaseTabComponent { click: () => { this.zone.run(() => { setTimeout(() => { - this.hterm.copySelectionToClipboard() + this.termContainer.copySelection() + this.toastr.info('Copied') }) }) } @@ -225,117 +226,65 @@ export class TerminalTabComponent extends BaseTabComponent { ]) } - attachHTermHandlers (hterm: any) { - hterm.setWindowTitle = title => this.zone.run(() => this.setTitle(title)) - - const _setAlternateMode = hterm.setAlternateMode.bind(hterm) - hterm.setAlternateMode = (state) => { - _setAlternateMode(state) - this.alternateScreenActive$.next(state) - } - - const _copySelectionToClipboard = hterm.copySelectionToClipboard.bind(hterm) - hterm.copySelectionToClipboard = () => { - _copySelectionToClipboard() - this.toastr.info('Copied') - } - - hterm.primaryScreen_.syncSelectionCaret = () => null - hterm.alternateScreen_.syncSelectionCaret = () => null - hterm.primaryScreen_.terminal = hterm - hterm.alternateScreen_.terminal = hterm - - hterm.scrollPort_.onPaste_ = (event) => { - 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 === 'mousedown') { - if (event.which === 3) { - if (this.config.store.terminal.rightClick === 'menu') { - this.contextMenu.popup({ - x: event.pageX + this.content.nativeElement.getBoundingClientRect().left, - y: event.pageY + this.content.nativeElement.getBoundingClientRect().top, - async: true, - }) - } else if (this.config.store.terminal.rightClick === 'paste') { - this.paste() - } - event.preventDefault() - event.stopPropagation() - return - } - } - 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 - } - } - - const _measureCharacterSize = hterm.scrollPort_.measureCharacterSize.bind(hterm.scrollPort_) - hterm.scrollPort_.measureCharacterSize = () => { - let size = _measureCharacterSize() - size.height += this.config.store.terminal.linePadding - return size + detachTermContainerHandlers () { + for (let subscription of this.termContainerSubscriptions) { + subscription.unsubscribe() } + this.termContainerSubscriptions = [] } - 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) + attachTermContainerHandlers () { + this.detachTermContainerHandlers() + this.termContainerSubscriptions = [ + this.termContainer.title$.subscribe(title => this.zone.run(() => this.setTitle(title))), + + this.focused$.subscribe(() => this.termContainer.enableResizing = true), + this.blurred$.subscribe(() => this.termContainer.enableResizing = false), + + this.termContainer.mouseEvent$.subscribe(event => { + if (event.type === 'mousedown') { + if (event.which === 3) { + if (this.config.store.terminal.rightClick === 'menu') { + this.contextMenu.popup({ + async: true, + }) + } else if (this.config.store.terminal.rightClick === 'paste') { + this.paste() + } + event.preventDefault() + event.stopPropagation() + return + } } - this.resize_.next(this.size) + if (event.type === 'mousewheel') { + if (event.ctrlKey || event.metaKey) { + if ((event as MouseWheelEvent).wheelDeltaY > 0) { + this.zoomIn() + } else { + this.zoomOut() + } + } else if (event.altKey) { + event.preventDefault() + let delta = Math.round((event as MouseWheelEvent).wheelDeltaY / 50) + this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta))) + } + } + }), + + this.termContainer.input$.subscribe(data => { + this.sendInput(data) + }), + + this.termContainer.resize$.subscribe(({columns, rows}) => { + console.log(`Resizing to ${columns}x${rows}`) + this.zone.run(() => { + this.size = { width: columns, height: rows } + if (this.session.open) { + this.session.resize(columns, rows) + } + }) }) - } + ] } sendInput (data: string) { @@ -343,111 +292,48 @@ export class TerminalTabComponent extends BaseTabComponent { } write (data: string) { - this.io.writeUTF8(data) + this.termContainer.write(data) } paste () { let data = this.electron.clipboard.readText() - data = this.hterm.keyboard.encode(data) - if (this.hterm.options_.bracketedPaste) { + data = hterm.lib.encodeUTF8(data) + if (this.config.store.terminal.bracketedPaste) { data = '\x1b[200~' + data + '\x1b[201~' } - data = data.replace(/\r\n/g, '\n') + data = data.replace(/\n/g, '\r') this.sendInput(data) } - clear () { - this.hterm.wipeContents() - this.hterm.onVTKeystroke('\f') - } - configure (): 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', config.terminal.copyOnSelect) - preferenceManager.set('alt-is-meta', config.terminal.altIsMeta) - preferenceManager.set('alt-sends-what', 'browser-key') - preferenceManager.set('alt-gr-mode', 'ctrl-alt') - preferenceManager.set('pass-alt-number', true) - preferenceManager.set('cursor-blink', config.terminal.cursorBlink) - preferenceManager.set('clear-selection-after-copy', true) + this.termContainer.configure(this.config.store) - 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) + 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 - // hterm can't parse "transparent" - preferenceManager.set('background-color', 'transparent') - } - 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() - this.hterm.setCursorBlink(config.terminal.cursorBlink) - if (config.terminal.cursorBlink) { - this.hterm.onCursorBlink_() } } zoomIn () { this.zoom++ - this.setFontSize() + this.termContainer.setZoom(this.zoom) } zoomOut () { this.zoom-- - this.setFontSize() + this.termContainer.setZoom(this.zoom) } resetZoom () { this.zoom = 0 - this.setFontSize() + this.termContainer.setZoom(this.zoom) } ngOnDestroy () { + this.detachTermContainerHandlers() this.config.enabledServices(this.decorators).forEach(decorator => { decorator.detach(this) }) @@ -455,13 +341,7 @@ export class TerminalTabComponent extends BaseTabComponent { 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 () { @@ -481,8 +361,4 @@ export class TerminalTabComponent extends BaseTabComponent { } 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)) - } } diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index 796a0125..e1fed22e 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -14,6 +14,7 @@ import { ColorPickerComponent } from './components/colorPicker.component' import { SessionsService, BaseSession } from './services/sessions.service' import { TerminalService } from './services/terminal.service' +import { TerminalContainersService } from './services/terminalContainers.service' import { ScreenPersistenceProvider } from './persistence/screen' import { TMuxPersistenceProvider } from './persistence/tmux' @@ -50,6 +51,7 @@ import { hterm } from './hterm' providers: [ SessionsService, TerminalService, + TerminalContainersService, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, @@ -123,4 +125,4 @@ export default class TerminalModule { } export * from './api' -export { TerminalService, BaseSession, TerminalTabComponent } +export { TerminalService, BaseSession, TerminalTabComponent, TerminalContainersService } diff --git a/terminus-terminal/src/pathDrop.ts b/terminus-terminal/src/pathDrop.ts index 5514d602..c5d24b8b 100644 --- a/terminus-terminal/src/pathDrop.ts +++ b/terminus-terminal/src/pathDrop.ts @@ -1,20 +1,25 @@ +import { Subscription } from 'rxjs' import { Injectable } from '@angular/core' import { TerminalDecorator } from './api' import { TerminalTabComponent } from './components/terminalTab.component' @Injectable() export class PathDropDecorator extends TerminalDecorator { + private subscriptions: Subscription[] = [] + attach (terminal: TerminalTabComponent): void { setTimeout(() => { - terminal.hterm.scrollPort_.document_.addEventListener('dragover', (event) => { - event.preventDefault() - }) - terminal.hterm.scrollPort_.document_.addEventListener('drop', (event) => { - for (let file of event.dataTransfer.files) { - this.injectPath(terminal, file.path) - } - event.preventDefault() - }) + this.subscriptions = [ + terminal.termContainer.dragOver$.subscribe(event => { + event.preventDefault() + }), + terminal.termContainer.drop$.subscribe(event => { + for (let file of event.dataTransfer.files as any) { + this.injectPath(terminal, file.path) + } + event.preventDefault() + }), + ] }) } @@ -25,6 +30,9 @@ export class PathDropDecorator extends TerminalDecorator { terminal.sendInput(path + ' ') } - // tslint:disable-next-line no-empty - detach (terminal: TerminalTabComponent): void { } + detach (terminal: TerminalTabComponent): void { + for (let s of this.subscriptions) { + s.unsubscribe() + } + } } diff --git a/terminus-terminal/src/services/sessions.service.ts b/terminus-terminal/src/services/sessions.service.ts index 055ed759..dfdc26e7 100644 --- a/terminus-terminal/src/services/sessions.service.ts +++ b/terminus-terminal/src/services/sessions.service.ts @@ -49,6 +49,7 @@ export abstract class BaseSession { this.initialDataBuffer = null } + abstract start (options: SessionOptions) abstract resize (columns, rows) abstract write (data) abstract kill (signal?: string) @@ -70,8 +71,7 @@ export abstract class BaseSession { export class Session extends BaseSession { private pty: any - constructor (options: SessionOptions) { - super() + start (options: SessionOptions) { this.name = options.name this.recoveryId = options.recoveryId @@ -200,7 +200,7 @@ export class Session extends BaseSession { @Injectable() export class SessionsService { - sessions: {[id: string]: Session} = {} + sessions: {[id: string]: BaseSession} = {} logger: Logger private lastID = 0 @@ -225,10 +225,10 @@ export class SessionsService { return options } - addSession (options: SessionOptions): Session { + addSession (session: BaseSession, options: SessionOptions) { this.lastID++ options.name = `session-${this.lastID}` - let session = new Session(options) + session.start(options) let persistence = this.getPersistence() session.destroyed$.pipe(first()).subscribe(() => { delete this.sessions[session.name] diff --git a/terminus-terminal/src/services/terminalContainers.service.ts b/terminus-terminal/src/services/terminalContainers.service.ts new file mode 100644 index 00000000..12f4d603 --- /dev/null +++ b/terminus-terminal/src/services/terminalContainers.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core' +import { TermContainer } from '../terminalContainers/termContainer' +import { HTermContainer } from '../terminalContainers/htermContainer' +import { BaseSession } from '../services/sessions.service' + +@Injectable() +export class TerminalContainersService { + private containers = new WeakMap() + + getContainer (session: BaseSession): TermContainer { + if (!this.containers.has(session)) { + this.containers.set(session, new HTermContainer()) + } + return this.containers.get(session) + } +} diff --git a/terminus-terminal/src/terminalContainers/htermContainer.ts b/terminus-terminal/src/terminalContainers/htermContainer.ts new file mode 100644 index 00000000..fa46c2d1 --- /dev/null +++ b/terminus-terminal/src/terminalContainers/htermContainer.ts @@ -0,0 +1,226 @@ +import { TermContainer } from './termContainer' +import { hterm, preferenceManager } from '../hterm' + +export class HTermContainer extends TermContainer { + term: any + io: any + private htermIframe: HTMLElement + private initialized = false + private configuredFontSize = 0 + private configuredLinePadding = 0 + private zoom = 0 + + attach (host: HTMLElement) { + if (!this.initialized) { + this.init() + this.initialized = true + this.term.decorate(host) + this.htermIframe = this.term.scrollPort_.iframe_ + } else { + host.appendChild(this.htermIframe) + } + } + + getSelection (): string { + return this.term.getSelectionText() + } + + copySelection () { + this.term.copySelectionToClipboard() + } + + clearSelection () { + this.term.getDocument().getSelection().removeAllRanges() + } + + focus () { + setTimeout(() => { + this.term.scrollPort_.resize() + this.term.scrollPort_.focus() + }, 100) + } + + write (data: string): void { + this.io.writeUTF8(data) + } + + clear (): void { + this.term.wipeContents() + this.term.onVTKeystroke('\f') + } + + configure (config: any): void { + this.configuredFontSize = config.terminal.fontSize + this.configuredLinePadding = config.terminal.linePadding + this.setFontSize() + + preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`) + 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', process.platform === 'darwin') + preferenceManager.set('copy-on-select', config.terminal.copyOnSelect) + preferenceManager.set('alt-is-meta', config.terminal.altIsMeta) + preferenceManager.set('alt-sends-what', 'browser-key') + preferenceManager.set('alt-gr-mode', 'ctrl-alt') + preferenceManager.set('pass-alt-number', true) + preferenceManager.set('cursor-blink', config.terminal.cursorBlink) + preferenceManager.set('clear-selection-after-copy', true) + + if (config.terminal.colorScheme.foreground) { + preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground) + } + + if (config.terminal.background === 'colorScheme') { + if (config.terminal.colorScheme.background) { + preferenceManager.set('background-color', config.terminal.colorScheme.background) + } + } else { + // hterm can't parse "transparent" + preferenceManager.set('background-color', 'transparent') + } + + 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.term.setCSS(css) + this.term.setBracketedPaste(config.terminal.bracketedPaste) + this.term.defaultCursorShape = { + block: hterm.hterm.Terminal.cursorShape.BLOCK, + underline: hterm.hterm.Terminal.cursorShape.UNDERLINE, + beam: hterm.hterm.Terminal.cursorShape.BEAM, + }[config.terminal.cursor] + this.term.applyCursorShape() + this.term.setCursorBlink(config.terminal.cursorBlink) + if (config.terminal.cursorBlink) { + this.term.onCursorBlink_() + } + } + + setZoom (zoom: number): void { + this.zoom = zoom + this.setFontSize() + } + + visualBell (): void { + const color = preferenceManager.get('background-color') + preferenceManager.set('background-color', 'rgba(128,128,128,.25)') + setTimeout(() => { + preferenceManager.set('background-color', color) + }, 125) + } + + private setFontSize () { + preferenceManager.set('font-size', this.configuredFontSize * Math.pow(1.1, this.zoom)) + } + + private init () { + this.term = new hterm.hterm.Terminal() + this.term.onTerminalReady = () => { + this.term.installKeyboard() + this.term.scrollPort_.setCtrlVPaste(true) + this.io = this.term.io.push() + this.io.onVTKeystroke = this.io.sendString = data => this.input.next(data) + this.io.onTerminalResize = (columns, rows) => { + console.log('hterm resize') + this.resize.next({ columns, rows }) + } + this.ready.next(null) + this.ready.complete() + + this.term.scrollPort_.document_.addEventListener('dragOver', event => { + this.dragOver.next(event) + }) + + this.term.scrollPort_.document_.addEventListener('drop', event => { + this.drop.next(event) + }) + } + this.term.setWindowTitle = title => this.title.next(title) + + const _setAlternateMode = this.term.setAlternateMode.bind(this.term) + this.term.setAlternateMode = (state) => { + _setAlternateMode(state) + this.alternateScreenActive.next(state) + } + + this.term.primaryScreen_.syncSelectionCaret = () => null + this.term.alternateScreen_.syncSelectionCaret = () => null + this.term.primaryScreen_.terminal = this.term + this.term.alternateScreen_.terminal = this.term + + this.term.scrollPort_.onPaste_ = (event) => { + event.preventDefault() + } + + const _resize = this.term.scrollPort_.resize.bind(this.term.scrollPort_) + this.term.scrollPort_.resize = () => { + if (this.enableResizing) { + _resize() + } + } + + const _onMouse = this.term.onMouse_.bind(this.term) + this.term.onMouse_ = (event) => { + this.mouseEvent.next(event) + if (event.type === 'mousedown' && event.which === 3) { + event.preventDefault() + event.stopPropagation() + return + } + if (event.type === 'mousewheel' && event.altKey) { + event.preventDefault() + } + _onMouse(event) + } + + this.term.ringBell = () => this.bell.next() + + for (let screen of [this.term.primaryScreen_, this.term.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 + } + } + + const _measureCharacterSize = this.term.scrollPort_.measureCharacterSize.bind(this.term.scrollPort_) + this.term.scrollPort_.measureCharacterSize = () => { + let size = _measureCharacterSize() + size.height += this.configuredLinePadding + return size + } + } +} diff --git a/terminus-terminal/src/terminalContainers/termContainer.ts b/terminus-terminal/src/terminalContainers/termContainer.ts new file mode 100644 index 00000000..0791fc45 --- /dev/null +++ b/terminus-terminal/src/terminalContainers/termContainer.ts @@ -0,0 +1,56 @@ +import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs' + +export abstract class TermContainer { + enableResizing = true + protected ready = new AsyncSubject() + protected title = new ReplaySubject(1) + protected alternateScreenActive = new BehaviorSubject(false) + protected mouseEvent = new Subject() + protected bell = new Subject() + protected contentUpdated = new Subject() + protected input = new Subject() + protected resize = new ReplaySubject<{columns: number, rows: number}>(1) + protected dragOver = new Subject() + protected drop = new Subject() + + get ready$ (): Observable { return this.ready } + get title$ (): Observable { return this.title } + get alternateScreenActive$ (): Observable { return this.alternateScreenActive } + get mouseEvent$ (): Observable { return this.mouseEvent } + get bell$ (): Observable { return this.bell } + get contentUpdated$ (): Observable { return this.contentUpdated } + get input$ (): Observable { return this.input } + get resize$ (): Observable<{columns: number, rows: number}> { return this.resize } + get dragOver$ (): Observable { return this.dragOver } + get drop$ (): Observable { return this.drop } + + abstract attach (host: HTMLElement): void + + destroy (): void { + for (let o of [ + this.ready, + this.title, + this.alternateScreenActive, + this.mouseEvent, + this.bell, + this.contentUpdated, + this.input, + this.resize, + this.dragOver, + this.drop, + ]) { + o.complete() + } + } + + abstract getSelection (): string + abstract copySelection (): void + abstract clearSelection (): void + abstract focus (): void + abstract write (data: string): void + abstract clear (): void + abstract visualBell (): void + + abstract configure (configStore: any): void + abstract setZoom (zoom: number): void +}