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
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
11 changed files with 333 additions and 280 deletions

View file

@ -19,10 +19,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "14.14.14", "@types/node": "14.14.14",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"binstring": "^0.2.1", "cli-spinner": "^0.2.10"
"buffer-replace": "^1.0.0",
"cli-spinner": "^0.2.10",
"hexer": "^1.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/animations": "^9.1.9", "@angular/animations": "^9.1.9",

View file

@ -1,15 +1,8 @@
import hexdump from 'hexer'
import colors from 'ansi-colors'
import binstring from 'binstring'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import bufferReplace from 'buffer-replace'
import { BaseSession } from 'tabby-terminal'
import { SerialPort } from 'serialport' import { SerialPort } from 'serialport'
import { Logger, Profile } from 'tabby-core' import { Logger, Profile } from 'tabby-core'
import { Subject, Observable, interval } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { debounce } from 'rxjs/operators' import { BaseSession, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
import { PassThrough, Readable, Writable } from 'stream'
export interface LoginScript { export interface LoginScript {
expect: string expect: string
@ -22,7 +15,7 @@ export interface SerialProfile extends Profile {
options: SerialProfileOptions options: SerialProfileOptions
} }
export interface SerialProfileOptions { export interface SerialProfileOptions extends StreamProcessingOptions {
port: string port: string
baudrate?: number baudrate?: number
databits?: number databits?: number
@ -34,10 +27,6 @@ export interface SerialProfileOptions {
xany?: boolean xany?: boolean
scripts?: LoginScript[] scripts?: LoginScript[]
color?: string color?: string
inputMode?: InputMode
inputNewlines?: NewlineMode
outputMode?: OutputMode
outputNewlines?: NewlineMode
} }
export const BAUD_RATES = [ export const BAUD_RATES = [
@ -49,10 +38,6 @@ export interface SerialPortInfo {
description?: string description?: string
} }
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 class SerialSession extends BaseSession { export class SerialSession extends BaseSession {
scripts?: LoginScript[] scripts?: LoginScript[]
serial: SerialPort serial: SerialPort
@ -60,38 +45,67 @@ export class SerialSession extends BaseSession {
get serviceMessage$ (): Observable<string> { return this.serviceMessage } get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>() private serviceMessage = new Subject<string>()
private inputReadline: ReadLine private streamProcessor: TerminalStreamProcessor
private inputPromptVisible = true
private inputReadlineInStream: Readable & Writable
private inputReadlineOutStream: Readable & Writable
constructor (public profile: SerialProfile) { constructor (public profile: SerialProfile) {
super() super()
this.scripts = profile.options.scripts ?? [] this.scripts = profile.options.scripts ?? []
this.streamProcessor = new TerminalStreamProcessor(profile.options)
this.streamProcessor.outputToSession$.subscribe(data => {
this.serial?.write(data.toString())
})
this.streamProcessor.outputToTerminal$.subscribe(data => {
this.emitOutput(data)
this.inputReadlineInStream = new PassThrough() const dataString = data.toString()
this.inputReadlineOutStream = new PassThrough()
this.inputReadline = createReadline({ if (this.scripts) {
input: this.inputReadlineInStream, let found = false
output: this.inputReadlineOutStream, for (const script of this.scripts) {
terminal: true, let match = false
prompt: this.profile.options.inputMode === 'readline-hex' ? 'hex> ' : '> ', let cmd = ''
} as any) if (script.isRegex) {
this.inputReadlineOutStream.on('data', data => { const re = new RegExp(script.expect, 'g')
this.emitOutput(Buffer.from(data)) if (re.test(dataString)) {
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else {
if (dataString.includes(script.expect)) {
cmd = script.send
match = true
found = true
}
}
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.serial.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
this.logger.debug('Skip optional script: ' + script.expect)
found = true
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
if (found) {
this.executeUnconditionalScripts()
}
}
}) })
this.inputReadline.on('line', line => {
this.onInput(Buffer.from(line + '\n'))
this.resetInputPrompt()
})
this.output$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled())
} }
async start (): Promise<void> { async start (): Promise<void> {
this.open = true this.open = true
this.serial.on('readable', () => { this.serial.on('readable', () => {
this.onOutput(this.serial.read()) this.streamProcessor.feedFromSession(this.serial.read())
}) })
this.serial.on('end', () => { this.serial.on('end', () => {
@ -105,22 +119,18 @@ export class SerialSession extends BaseSession {
} }
write (data: Buffer): void { write (data: Buffer): void {
if (this.profile.options.inputMode?.startsWith('readline')) { this.streamProcessor.feedFromTerminal(data)
this.inputReadlineInStream.write(data)
} else {
this.onInput(data)
}
} }
async destroy (): Promise<void> { async destroy (): Promise<void> {
this.streamProcessor.close()
this.serviceMessage.complete() this.serviceMessage.complete()
this.inputReadline.close()
await super.destroy() await super.destroy()
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
resize (_, __) { resize (_, __) {
this.inputReadlineOutStream.emit('resize') this.streamProcessor.resize()
} }
kill (_?: string): void { kill (_?: string): void {
@ -148,118 +158,6 @@ export class SerialSession extends BaseSession {
return null return null
} }
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)
}
private onInput (data: Buffer) {
if (this.profile.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.profile.options.inputNewlines)
if (this.serial) {
this.serial.write(data.toString())
}
}
private onOutputSettled () {
if (this.profile.options.inputMode?.startsWith('readline') && !this.inputPromptVisible) {
this.resetInputPrompt()
}
}
private resetInputPrompt () {
this.emitOutput(Buffer.from('\r\n'))
this.inputReadline.prompt(true)
this.inputPromptVisible = true
}
private onOutput (data: Buffer) {
const dataString = data.toString()
if (this.profile.options.inputMode?.startsWith('readline')) {
if (this.inputPromptVisible) {
clearLine(this.inputReadlineOutStream, 0)
this.inputPromptVisible = false
}
}
data = this.replaceNewlines(data, this.profile.options.outputNewlines)
if (this.profile.options.outputMode === 'hex') {
this.emitOutput(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.emitOutput(data)
}
if (this.scripts) {
let found = false
for (const script of this.scripts) {
let match = false
let cmd = ''
if (script.isRegex) {
const re = new RegExp(script.expect, 'g')
if (re.test(dataString)) {
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else {
if (dataString.includes(script.expect)) {
cmd = script.send
match = true
found = true
}
}
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.serial.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
this.logger.debug('Skip optional script: ' + script.expect)
found = true
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
if (found) {
this.executeUnconditionalScripts()
}
}
}
private executeUnconditionalScripts () { private executeUnconditionalScripts () {
if (this.scripts) { if (this.scripts) {
for (const script of this.scripts) { for (const script of this.scripts) {

View file

@ -8,7 +8,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
label Device label Device
input.form-control( input.form-control(
type='text', type='text',
alwaysShowTypeahead, alwaysVisibleTypeahead,
[(ngModel)]='profile.options.port', [(ngModel)]='profile.options.port',
[ngbTypeahead]='portsAutocomplete', [ngbTypeahead]='portsAutocomplete',
[resultFormatter]='portsFormatter' [resultFormatter]='portsFormatter'
@ -19,65 +19,13 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
label Baud Rate label Baud Rate
input.form-control( input.form-control(
type='number', type='number',
alwaysShowTypeahead, alwaysVisibleTypeahead,
placeholder='Ask every time', placeholder='Ask every time',
[(ngModel)]='profile.options.baudrate', [(ngModel)]='profile.options.baudrate',
[ngbTypeahead]='baudratesAutocomplete' [ngbTypeahead]='baudratesAutocomplete'
) )
.form-line stream-processing-settings([options]='profile.options')
.header
.title Input mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getInputModeName(profile.options.inputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of inputModes',
(click)='profile.options.inputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.form-line
.header
.title Input newlines
select.form-control(
[(ngModel)]='profile.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(profile.options.outputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of outputModes',
(click)='profile.options.outputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.form-line
.header
.title Output newlines
select.form-control(
[(ngModel)]='profile.options.outputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
li(ngbNavItem) li(ngbNavItem)
a(ngbNavLink) Advanced a(ngbNavLink) Advanced

View file

@ -12,36 +12,12 @@ import { SerialService } from '../services/serial.service'
export class SerialProfileSettingsComponent implements ProfileSettingsComponent { export class SerialProfileSettingsComponent implements ProfileSettingsComponent {
profile: SerialProfile profile: SerialProfile
foundPorts: SerialPortInfo[] foundPorts: SerialPortInfo[]
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' },
]
constructor ( constructor (
private platform: PlatformService, private platform: PlatformService,
private serial: SerialService, private serial: SerialService,
) { } ) { }
getInputModeName (key) {
return this.inputModes.find(x => x.key === key)?.name
}
getOutputModeName (key) {
return this.outputModes.find(x => x.key === key)?.name
}
portsAutocomplete = text$ => text$.pipe(map(() => { portsAutocomplete = text$ => text$.pipe(map(() => {
return this.foundPorts.map(x => x.name) return this.foundPorts.map(x => x.name)
})) }))

View file

@ -7,52 +7,12 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==
ansi-color@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a"
integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o=
ansi-colors@^4.1.1: ansi-colors@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
binstring@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/binstring/-/binstring-0.2.1.tgz#8a174d301f6d54efda550dd98bb4cb524eacd75d"
integrity sha1-ihdNMB9tVO/aVQ3Zi7TLUk6s110=
buffer-replace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-replace/-/buffer-replace-1.0.0.tgz#bc427c40af4c1f06d6933dede57110acba8ade54"
integrity sha1-vEJ8QK9MHwbWkz3t5XEQrLqK3lQ=
cli-spinner@^0.2.10: cli-spinner@^0.2.10:
version "0.2.10" version "0.2.10"
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47" resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"
integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q== integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==
hexer@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653"
integrity sha1-uGzoCFmOip0YksVx887dhvyfBlM=
dependencies:
ansi-color "^0.2.1"
minimist "^1.1.0"
process "^0.10.0"
xtend "^4.0.0"
minimist@^1.1.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
process@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725"
integrity sha1-hCRXzFHP7XLcd1r+6vuMYDQ3JyU=
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==

View file

@ -31,6 +31,9 @@
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"utils-decorators": "^1.8.1", "utils-decorators": "^1.8.1",
"xterm": "^4.9.0-beta.7", "xterm": "^4.9.0-beta.7",
"binstring": "^0.2.1",
"buffer-replace": "^1.0.0",
"hexer": "^1.5.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0",
"xterm-addon-ligatures": "^0.5.0", "xterm-addon-ligatures": "^0.5.0",
"xterm-addon-search": "^0.8.0", "xterm-addon-search": "^0.8.0",

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 { ColorPickerComponent } from './components/colorPicker.component'
import { ColorSchemePreviewComponent } from './components/colorSchemePreview.component' import { ColorSchemePreviewComponent } from './components/colorSchemePreview.component'
import { SearchPanelComponent } from './components/searchPanel.component' import { SearchPanelComponent } from './components/searchPanel.component'
import { StreamProcessingSettingsComponent } from './components/streamProcessingSettings.component'
import { TerminalFrontendService } from './services/terminalFrontend.service' import { TerminalFrontendService } from './services/terminalFrontend.service'
@ -70,10 +71,12 @@ import { TerminalCLIHandler } from './cli'
ColorSchemeSettingsTabComponent, ColorSchemeSettingsTabComponent,
TerminalSettingsTabComponent, TerminalSettingsTabComponent,
SearchPanelComponent, SearchPanelComponent,
StreamProcessingSettingsComponent,
] as any[], ] as any[],
exports: [ exports: [
ColorPickerComponent, ColorPickerComponent,
SearchPanelComponent, SearchPanelComponent,
StreamProcessingSettingsComponent,
], ],
}) })
export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class 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 { Frontend, XTermFrontend, XTermWebGLFrontend, HTermFrontend }
export { BaseTerminalTabComponent } from './api/baseTerminalTab.component' export { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
export * from './api/interfaces' export * from './api/interfaces'
export * from './api/streamProcessing'
export * from './session' export * from './session'

View file

@ -12,6 +12,11 @@
resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2" resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2"
integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g== integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g==
ansi-color@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a"
integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o=
ansi-colors@^4.1.1: ansi-colors@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -29,6 +34,16 @@ available-typed-arrays@^1.0.2:
dependencies: dependencies:
array-filter "^1.0.0" array-filter "^1.0.0"
binstring@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/binstring/-/binstring-0.2.1.tgz#8a174d301f6d54efda550dd98bb4cb524eacd75d"
integrity sha1-ihdNMB9tVO/aVQ3Zi7TLUk6s110=
buffer-replace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-replace/-/buffer-replace-1.0.0.tgz#bc427c40af4c1f06d6933dede57110acba8ade54"
integrity sha1-vEJ8QK9MHwbWkz3t5XEQrLqK3lQ=
call-bind@^1.0.0, call-bind@^1.0.2: call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@ -214,6 +229,16 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hexer@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653"
integrity sha1-uGzoCFmOip0YksVx887dhvyfBlM=
dependencies:
ansi-color "^0.2.1"
minimist "^1.1.0"
process "^0.10.0"
xtend "^4.0.0"
hterm-umdjs@1.4.1: hterm-umdjs@1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957" resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957"
@ -338,6 +363,11 @@ lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" yallist "^4.0.0"
minimist@^1.1.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
object-inspect@^1.8.0: object-inspect@^1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
@ -391,6 +421,11 @@ printj@~1.1.0:
resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
process@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725"
integrity sha1-hCRXzFHP7XLcd1r+6vuMYDQ3JyU=
promise-stream-reader@^1.0.1: promise-stream-reader@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649" resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
@ -535,6 +570,11 @@ which-typed-array@^1.1.2:
has-symbols "^1.0.1" has-symbols "^1.0.1"
is-typed-array "^1.1.3" is-typed-array "^1.1.3"
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm-addon-fit@^0.5.0: xterm-addon-fit@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"