moved stream processing into tabby-terminal

This commit is contained in:
Eugene Pankov
2021-07-04 14:05:25 +02:00
parent cbbd38ca83
commit 9155104662
11 changed files with 333 additions and 280 deletions

View File

@@ -0,0 +1,137 @@
import hexdump from 'hexer'
import bufferReplace from 'buffer-replace'
import colors from 'ansi-colors'
import binstring from 'binstring'
import { Subject, Observable, interval } from 'rxjs'
import { debounce } from 'rxjs/operators'
import { PassThrough, Readable, Writable } from 'stream'
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
export type InputMode = null | 'readline' | 'readline-hex' // eslint-disable-line @typescript-eslint/no-type-alias
export type OutputMode = null | 'hex' // eslint-disable-line @typescript-eslint/no-type-alias
export type NewlineMode = null | 'cr' | 'lf' | 'crlf' // eslint-disable-line @typescript-eslint/no-type-alias
export interface StreamProcessingOptions {
inputMode?: InputMode
inputNewlines?: NewlineMode
outputMode?: OutputMode
outputNewlines?: NewlineMode
}
export class TerminalStreamProcessor {
get outputToSession$ (): Observable<Buffer> { return this.outputToSession }
get outputToTerminal$ (): Observable<Buffer> { return this.outputToTerminal }
protected outputToSession = new Subject<Buffer>()
protected outputToTerminal = new Subject<Buffer>()
private inputReadline: ReadLine
private inputPromptVisible = true
private inputReadlineInStream: Readable & Writable
private inputReadlineOutStream: Readable & Writable
constructor (private options: StreamProcessingOptions) {
this.inputReadlineInStream = new PassThrough()
this.inputReadlineOutStream = new PassThrough()
this.inputReadline = createReadline({
input: this.inputReadlineInStream,
output: this.inputReadlineOutStream,
terminal: true,
prompt: this.options.inputMode === 'readline-hex' ? 'hex> ' : '> ',
} as any)
this.inputReadlineOutStream.on('data', data => {
this.outputToTerminal.next(Buffer.from(data))
})
this.inputReadline.on('line', line => {
this.onTerminalInput(Buffer.from(line + '\n'))
this.resetInputPrompt()
})
this.outputToTerminal$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled())
}
feedFromSession (data: Buffer): void {
if (this.options.inputMode?.startsWith('readline')) {
if (this.inputPromptVisible) {
clearLine(this.inputReadlineOutStream, 0)
this.inputPromptVisible = false
}
}
data = this.replaceNewlines(data, this.options.outputNewlines)
if (this.options.outputMode === 'hex') {
this.outputToTerminal.next(Buffer.concat([
Buffer.from('\r\n'),
Buffer.from(hexdump(data, {
group: 1,
gutter: 4,
divide: colors.gray(' '),
emptyHuman: colors.gray(''),
}).replace(/\n/g, '\r\n')),
Buffer.from('\r\n\n'),
]))
} else {
this.outputToTerminal.next(data)
}
}
feedFromTerminal (data: Buffer): void {
if (this.options.inputMode?.startsWith('readline')) {
this.inputReadlineInStream.write(data)
} else {
this.onTerminalInput(data)
}
}
resize (): void {
this.inputReadlineOutStream.emit('resize')
}
close (): void {
this.inputReadline.close()
this.outputToSession.complete()
this.outputToTerminal.complete()
}
private onTerminalInput (data: Buffer) {
if (this.options.inputMode === 'readline-hex') {
const tokens = data.toString().split(/\s/g)
data = Buffer.concat(tokens.filter(t => !!t).map(t => {
if (t.startsWith('0x')) {
t = t.substring(2)
}
return binstring(t, { 'in': 'hex' })
}))
}
data = this.replaceNewlines(data, this.options.inputNewlines)
this.outputToSession.next(data)
}
private onOutputSettled () {
if (this.options.inputMode?.startsWith('readline') && !this.inputPromptVisible) {
this.resetInputPrompt()
}
}
private resetInputPrompt () {
this.outputToTerminal.next(Buffer.from('\r\n'))
this.inputReadline.prompt(true)
this.inputPromptVisible = true
}
private replaceNewlines (data: Buffer, mode?: NewlineMode): Buffer {
if (!mode) {
return data
}
data = bufferReplace(data, '\r\n', '\n')
data = bufferReplace(data, '\r', '\n')
const replacement = {
strip: '',
cr: '\r',
lf: '\n',
crlf: '\r\n',
}[mode]
return bufferReplace(data, '\n', replacement)
}
}

View File

@@ -0,0 +1,53 @@
.form-line
.header
.title Input mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getInputModeName(options.inputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of inputModes',
(click)='options.inputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.form-line
.header
.title Input newlines
select.form-control(
[(ngModel)]='options.inputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
.form-line
.header
.title Output mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getOutputModeName(options.outputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of outputModes',
(click)='options.outputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.form-line
.header
.title Output newlines
select.form-control(
[(ngModel)]='options.outputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}

View File

@@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input } from '@angular/core'
import { StreamProcessingOptions } from '../api/streamProcessing'
/** @hidden */
@Component({
selector: 'stream-processing-settings',
template: require('./streamProcessingSettings.component.pug'),
})
export class StreamProcessingSettingsComponent {
@Input() options: StreamProcessingOptions
inputModes = [
{ key: null, name: 'Normal', description: 'Input is sent as you type' },
{ key: 'readline', name: 'Line by line', description: 'Line editor, input is sent after you press Enter' },
{ key: 'readline-hex', name: 'Hexadecimal', description: 'Send bytes by typing in hex values' },
]
outputModes = [
{ key: null, name: 'Normal', description: 'Output is shown as it is received' },
{ key: 'hex', name: 'Hexadecimal', description: 'Output is shown as a hexdump' },
]
newlineModes = [
{ key: null, name: 'Keep' },
{ key: 'strip', name: 'Strip' },
{ key: 'cr', name: 'Force CR' },
{ key: 'lf', name: 'Force LF' },
{ key: 'crlf', name: 'Force CRLF' },
]
getInputModeName (key) {
return this.inputModes.find(x => x.key === key)?.name
}
getOutputModeName (key) {
return this.outputModes.find(x => x.key === key)?.name
}
}

View File

@@ -13,6 +13,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
import { ColorPickerComponent } from './components/colorPicker.component'
import { ColorSchemePreviewComponent } from './components/colorSchemePreview.component'
import { SearchPanelComponent } from './components/searchPanel.component'
import { StreamProcessingSettingsComponent } from './components/streamProcessingSettings.component'
import { TerminalFrontendService } from './services/terminalFrontend.service'
@@ -70,10 +71,12 @@ import { TerminalCLIHandler } from './cli'
ColorSchemeSettingsTabComponent,
TerminalSettingsTabComponent,
SearchPanelComponent,
StreamProcessingSettingsComponent,
] as any[],
exports: [
ColorPickerComponent,
SearchPanelComponent,
StreamProcessingSettingsComponent,
],
})
export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
@@ -111,4 +114,5 @@ export { TerminalFrontendService, TerminalDecorator, TerminalContextMenuItemProv
export { Frontend, XTermFrontend, XTermWebGLFrontend, HTermFrontend }
export { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
export * from './api/interfaces'
export * from './api/streamProcessing'
export * from './session'