mirror of
https://github.com/Eugeny/tabby
synced 2024-12-13 23:02:41 +00:00
moved stream processing into tabby-terminal
This commit is contained in:
parent
cbbd38ca83
commit
9155104662
11 changed files with 333 additions and 280 deletions
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -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==
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
137
tabby-terminal/src/api/streamProcessing.ts
Normal file
137
tabby-terminal/src/api/streamProcessing.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue