readline input mode for serial terminals - #3099, #2661

This commit is contained in:
Eugene Pankov 2021-01-31 19:33:30 +01:00
parent 5bd1bfd565
commit 73574374f0
8 changed files with 116 additions and 22 deletions

View File

@ -95,3 +95,7 @@ input[type=range] {
&::-moz-range-track { @include track(); }
&::-ms-track { @include track(); }
}
a[ngbdropdownitem] {
cursor: pointer;
}

View File

@ -2,7 +2,10 @@ import stripAnsi from 'strip-ansi'
import { BaseSession } from 'terminus-terminal'
import { SerialPort } from 'serialport'
import { Logger } from 'terminus-core'
import { Subject, Observable } from 'rxjs'
import { Subject, Observable, interval } from 'rxjs'
import { debounce } from 'rxjs/operators'
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
import { PassThrough, Readable, Writable } from 'stream'
export interface LoginScript {
expect: string
@ -24,6 +27,7 @@ export interface SerialConnection {
xany: boolean
scripts?: LoginScript[]
color?: string
inputMode?: InputMode
}
export const BAUD_RATES = [
@ -35,6 +39,8 @@ export interface SerialPortInfo {
description?: string
}
export type InputMode = null | 'readline'
export class SerialSession extends BaseSession {
scripts?: LoginScript[]
serial: SerialPort
@ -42,17 +48,38 @@ export class SerialSession extends BaseSession {
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>()
private inputReadline: ReadLine
private inputPromptVisible = true
private inputReadlineInStream: Readable & Writable
private inputReadlineOutStream: Readable & Writable
constructor (public connection: SerialConnection) {
super()
this.scripts = connection.scripts ?? []
this.inputReadlineInStream = new PassThrough()
this.inputReadlineOutStream = new PassThrough()
this.inputReadline = createReadline({
input: this.inputReadlineInStream,
output: this.inputReadlineOutStream,
terminal: true,
} as any)
this.inputReadlineOutStream.on('data', data => {
if (this.connection.inputMode == 'readline') {
this.emitOutput(data)
}
})
this.inputReadline.on('line', line => {
this.onInput(new Buffer(line + '\n'))
})
this.output$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled())
}
async start (): Promise<void> {
this.open = true
this.serial.on('readable', () => {
this.onData(this.serial.read())
this.onOutput(this.serial.read())
})
this.serial.on('end', () => {
@ -66,18 +93,23 @@ export class SerialSession extends BaseSession {
}
write (data: Buffer): void {
if (this.serial) {
this.serial.write(data.toString())
if (this.connection.inputMode == 'readline') {
this.inputReadlineInStream.write(data)
} else {
this.onInput(data)
}
}
async destroy (): Promise<void> {
this.serviceMessage.complete()
this.inputReadline.close()
await super.destroy()
}
// 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')
}
kill (_?: string): void {
this.serial.close()
@ -104,8 +136,33 @@ export class SerialSession extends BaseSession {
return null
}
private onData (data: Buffer) {
private onInput (data: Buffer) {
if (this.serial) {
this.serial.write(data.toString())
}
}
private onOutputSettled () {
if (this.connection.inputMode == 'readline' && !this.inputPromptVisible) {
this.resetInputPrompt()
}
}
private resetInputPrompt () {
this.emitOutput(new Buffer('\r\n'))
this.inputReadline.prompt(true)
this.inputPromptVisible = true
}
private onOutput (data: Buffer) {
const dataString = data.toString()
if (this.connection.inputMode == 'readline') {
if (this.inputPromptVisible) {
clearLine(this.inputReadlineOutStream, 0)
this.inputPromptVisible = false
}
}
this.emitOutput(data)
if (this.scripts) {

View File

@ -11,6 +11,8 @@
[(ngModel)]='connection.name',
)
.row
.col-6
.form-group
label Path
input.form-control(
@ -20,6 +22,7 @@
[resultFormatter]='portsFormatter',
)
.col-6
.form-group
label Baud Rate
select.form-control(
@ -27,6 +30,25 @@
)
option([value]='x', *ngFor='let x of baudRates') {{x}}
.form-line
.header
.title Input mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getInputModeName(connection.inputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of inputModes',
(click)='connection.inputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
ngb-tab(id='advanced')
ng-template(ngbTabTitle) Advanced
ng-template(ngbTabContent)

View File

@ -15,6 +15,10 @@ export class EditConnectionModalComponent {
connection: SerialConnection
foundPorts: SerialPortInfo[]
baudRates = BAUD_RATES
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' },
]
constructor (
private modalInstance: NgbActiveModal,
@ -24,6 +28,10 @@ export class EditConnectionModalComponent {
) {
}
getInputModeName (key) {
return this.inputModes.find(x => x.key === key)?.name
}
portsAutocomplete = text$ => text$.pipe(map(() => {
return this.foundPorts.map(x => x.name)
}))

View File

@ -34,6 +34,7 @@ export class SerialSettingsTabComponent {
xany: false,
xoff: false,
xon: false,
inputMode: null
}
const modal = this.ngbModal.open(EditConnectionModalComponent)

View File

@ -114,7 +114,7 @@ export class SerialService {
options.push({
name: 'Manage connections',
icon: 'cog',
callback: () => this.app.openNewTab(SettingsTabComponent, { activeTab: 'serial' }),
callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'serial' }),
})
options.push({

View File

@ -50,6 +50,8 @@ module.exports = {
'path',
'ngx-toastr',
'serialport',
'readline',
'stream',
'windows-process-tree/build/Release/windows_process_tree.node',
/^rxjs/,
/^@angular/,

View File

@ -383,7 +383,7 @@ export class SSHService {
options.push({
name: 'Manage connections',
icon: 'cog',
callback: () => this.app.openNewTab(SettingsTabComponent, { activeTab: 'ssh' }),
callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'ssh' }),
})
options.push({