new profile system

This commit is contained in:
Eugene Pankov
2021-07-04 12:23:27 +02:00
parent 38b7e44f64
commit 92b34fbc08
104 changed files with 2029 additions and 2205 deletions

View File

@@ -5,7 +5,7 @@ import stripAnsi from 'strip-ansi'
import bufferReplace from 'buffer-replace'
import { BaseSession } from 'tabby-terminal'
import { SerialPort } from 'serialport'
import { Logger } from 'tabby-core'
import { Logger, Profile } from 'tabby-core'
import { Subject, Observable, interval } from 'rxjs'
import { debounce } from 'rxjs/operators'
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
@@ -18,17 +18,20 @@ export interface LoginScript {
optional?: boolean
}
export interface SerialConnection {
name: string
export interface SerialProfile extends Profile {
options: SerialProfileOptions
}
export interface SerialProfileOptions {
port: string
baudrate: number
databits: number
stopbits: number
parity: string
rtscts: boolean
xon: boolean
xoff: boolean
xany: boolean
baudrate?: number
databits?: number
stopbits?: number
parity?: string
rtscts?: boolean
xon?: boolean
xoff?: boolean
xany?: boolean
scripts?: LoginScript[]
color?: string
inputMode?: InputMode
@@ -62,9 +65,9 @@ export class SerialSession extends BaseSession {
private inputReadlineInStream: Readable & Writable
private inputReadlineOutStream: Readable & Writable
constructor (public connection: SerialConnection) {
constructor (public profile: SerialProfile) {
super()
this.scripts = connection.scripts ?? []
this.scripts = profile.options.scripts ?? []
this.inputReadlineInStream = new PassThrough()
this.inputReadlineOutStream = new PassThrough()
@@ -72,7 +75,7 @@ export class SerialSession extends BaseSession {
input: this.inputReadlineInStream,
output: this.inputReadlineOutStream,
terminal: true,
prompt: this.connection.inputMode === 'readline-hex' ? 'hex> ' : '> ',
prompt: this.profile.options.inputMode === 'readline-hex' ? 'hex> ' : '> ',
} as any)
this.inputReadlineOutStream.on('data', data => {
this.emitOutput(Buffer.from(data))
@@ -102,7 +105,7 @@ export class SerialSession extends BaseSession {
}
write (data: Buffer): void {
if (this.connection.inputMode?.startsWith('readline')) {
if (this.profile.options.inputMode?.startsWith('readline')) {
this.inputReadlineInStream.write(data)
} else {
this.onInput(data)
@@ -161,7 +164,7 @@ export class SerialSession extends BaseSession {
}
private onInput (data: Buffer) {
if (this.connection.inputMode === 'readline-hex') {
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')) {
@@ -171,14 +174,14 @@ export class SerialSession extends BaseSession {
}))
}
data = this.replaceNewlines(data, this.connection.inputNewlines)
data = this.replaceNewlines(data, this.profile.options.inputNewlines)
if (this.serial) {
this.serial.write(data.toString())
}
}
private onOutputSettled () {
if (this.connection.inputMode?.startsWith('readline') && !this.inputPromptVisible) {
if (this.profile.options.inputMode?.startsWith('readline') && !this.inputPromptVisible) {
this.resetInputPrompt()
}
}
@@ -192,16 +195,16 @@ export class SerialSession extends BaseSession {
private onOutput (data: Buffer) {
const dataString = data.toString()
if (this.connection.inputMode?.startsWith('readline')) {
if (this.profile.options.inputMode?.startsWith('readline')) {
if (this.inputPromptVisible) {
clearLine(this.inputReadlineOutStream, 0)
this.inputPromptVisible = false
}
}
data = this.replaceNewlines(data, this.connection.outputNewlines)
data = this.replaceNewlines(data, this.profile.options.outputNewlines)
if (this.connection.outputMode === 'hex') {
if (this.profile.options.outputMode === 'hex') {
this.emitOutput(Buffer.concat([
Buffer.from('\r\n'),
Buffer.from(hexdump(data, {
@@ -271,8 +274,3 @@ export class SerialSession extends BaseSession {
}
}
}
export interface SerialConnectionGroup {
name: string
connections: SerialConnection[]
}

View File

@@ -1,36 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable, Injector } from '@angular/core'
import { HotkeysService, ToolbarButtonProvider, ToolbarButton } from 'tabby-core'
import { SerialService } from './services/serial.service'
/** @hidden */
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
private injector: Injector,
hotkeys: HotkeysService,
) {
super()
hotkeys.matchedHotkey.subscribe(async (hotkey: string) => {
if (hotkey === 'serial') {
this.activate()
}
})
}
activate () {
this.injector.get(SerialService).showConnectionSelector()
}
provide (): ToolbarButton[] {
return [{
icon: require('./icons/serial.svg'),
weight: 5,
title: 'Serial connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: () => {
this.activate()
},
}]
}
}

View File

@@ -1,30 +0,0 @@
import { Injectable } from '@angular/core'
import { CLIHandler, CLIEvent, ConfigService } from 'tabby-core'
import { SerialService } from './services/serial.service'
@Injectable()
export class SerialCLIHandler extends CLIHandler {
firstMatchOnly = true
priority = 0
constructor (
private serial: SerialService,
private config: ConfigService,
) {
super()
}
async handle (event: CLIEvent): Promise<boolean> {
const op = event.argv._[0]
if (op === 'connect-serial') {
const connection = this.config.store.serial.connections.find(x => x.name === event.argv.connectionName)
if (connection) {
this.serial.connect(connection)
}
return true
}
return false
}
}

View File

@@ -1,200 +0,0 @@
.modal-body
ul.nav-tabs(ngbNav, #nav='ngbNav')
li(ngbNavItem)
a(ngbNavLink) General
ng-template(ngbNavContent)
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='connection.name',
)
.row
.col-6
.form-group
label Path
input.form-control(
type='text',
[(ngModel)]='connection.port',
[ngbTypeahead]='portsAutocomplete',
[resultFormatter]='portsFormatter',
)
.col-6
.form-group
label Baud Rate
input.form-control(
type='number',
[(ngModel)]='connection.baudrate',
[ngbTypeahead]='baudratesAutocomplete',
)
.row
.col-6
.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}}
.col-6
.form-line
.header
.title Input newlines
select.form-control(
[(ngModel)]='connection.inputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
.row
.col-6
.form-line
.header
.title Output mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getOutputModeName(connection.outputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of outputModes',
(click)='connection.outputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.col-6
.form-line
.header
.title Output newlines
select.form-control(
[(ngModel)]='connection.outputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
li(ngbNavItem)
a(ngbNavLink) Advanced
ng-template(ngbNavContent)
.form-line
.header
.title Tab color
input.form-control(
type='text',
autofocus,
[(ngModel)]='connection.color',
placeholder='#000000'
)
.form-line
.header
.title DataBits
input.form-control(
type='number',
placeholder='8',
[(ngModel)]='connection.databits',
)
.form-line
.header
.title StopBits
input.form-control(
type='number',
placeholder='1',
[(ngModel)]='connection.stopbits',
)
.form-line
.header
.title Parity
input.form-control(
type='text',
[(ngModel)]='connection.parity',
placeholder='none'
)
.form-line
.header
.title RTSCTS
toggle([(ngModel)]='connection.rtscts')
.form-line
.header
.title Xon
toggle([(ngModel)]='connection.xon')
.form-line
.header
.title Xoff
toggle([(ngModel)]='connection.xoff')
.form-line
.header
.title Xany
toggle([(ngModel)]='connection.xany')
li(ngbNavItem)
a(ngbNavLink) Login scripts
ng-template(ngbNavContent)
table(*ngIf='connection.scripts.length > 0')
tr
th String to expect
th String to be sent
th.pl-2 Regex
th.pl-2 Optional
th.pl-2 Actions
tr(*ngFor='let script of connection.scripts')
td.pr-2
input.form-control(
type='text',
[(ngModel)]='script.expect'
)
td
input.form-control(
type='text',
[(ngModel)]='script.send'
)
td.pl-2
checkbox(
[(ngModel)]='script.isRegex',
)
td.pl-2
checkbox(
[(ngModel)]='script.optional',
)
td.pl-2
.input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
i.fas.fa-arrow-up
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
i.fas.fa-arrow-down
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
i.fas.fa-trash
button.btn.btn-outline-info.mt-2((click)='addScript()')
i.fas.fa-plus
span New item
div([ngbNavOutlet]='nav')
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save
button.btn.btn-outline-danger((click)='cancel()') Cancel

View File

@@ -0,0 +1,171 @@
ul.nav-tabs(ngbNav, #nav='ngbNav')
li(ngbNavItem)
a(ngbNavLink) General
ng-template(ngbNavContent)
.row
.col-6
.form-group
label Device
input.form-control(
type='text',
[(ngModel)]='profile.options.port',
[ngbTypeahead]='portsAutocomplete',
[resultFormatter]='portsFormatter',
)
.col-6
.form-group
label Baud Rate
input.form-control(
type='number',
[(ngModel)]='profile.options.baudrate',
[ngbTypeahead]='baudratesAutocomplete',
)
.form-line
.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)
a(ngbNavLink) Advanced
ng-template(ngbNavContent)
.form-line
.header
.title Data bits
input.form-control(
type='number',
placeholder='8',
[(ngModel)]='profile.options.databits',
)
.form-line
.header
.title Stop bits
input.form-control(
type='number',
placeholder='1',
[(ngModel)]='profile.options.stopbits',
)
.form-line
.header
.title Parity
input.form-control(
type='text',
[(ngModel)]='profile.options.parity',
placeholder='none'
)
.form-line
.header
.title RTS / CTS
toggle([(ngModel)]='profile.options.rtscts')
.form-line
.header
.title XON
toggle([(ngModel)]='profile.options.xon')
.form-line
.header
.title XOFF
toggle([(ngModel)]='profile.options.xoff')
.form-line
.header
.title Xany
toggle([(ngModel)]='profile.options.xany')
li(ngbNavItem)
a(ngbNavLink) Login scripts
ng-template(ngbNavContent)
table(*ngIf='profile.options.scripts.length > 0')
tr
th String to expect
th String to be sent
th.pl-2 Regex
th.pl-2 Optional
th.pl-2 Actions
tr(*ngFor='let script of profile.options.scripts')
td.pr-2
input.form-control(
type='text',
[(ngModel)]='script.expect'
)
td
input.form-control(
type='text',
[(ngModel)]='script.send'
)
td.pl-2
checkbox(
[(ngModel)]='script.isRegex',
)
td.pl-2
checkbox(
[(ngModel)]='script.optional',
)
td.pl-2
.input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
i.fas.fa-arrow-up
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
i.fas.fa-arrow-down
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
i.fas.fa-trash
button.btn.btn-outline-info.mt-2((click)='addScript()')
i.fas.fa-plus
span New item
div([ngbNavOutlet]='nav')

View File

@@ -1,17 +1,16 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
import { PlatformService } from 'tabby-core'
import { SerialConnection, LoginScript, SerialPortInfo, BAUD_RATES } from '../api'
import { PlatformService, ProfileSettingsComponent } from 'tabby-core'
import { LoginScript, SerialPortInfo, BAUD_RATES, SerialProfile } from '../api'
import { SerialService } from '../services/serial.service'
/** @hidden */
@Component({
template: require('./editConnectionModal.component.pug'),
template: require('./serialProfileSettings.component.pug'),
})
export class EditConnectionModalComponent {
connection: SerialConnection
export class SerialProfileSettingsComponent implements ProfileSettingsComponent {
profile: SerialProfile
foundPorts: SerialPortInfo[]
inputModes = [
{ key: null, name: 'Normal', description: 'Input is sent as you type' },
@@ -31,11 +30,9 @@ export class EditConnectionModalComponent {
]
constructor (
private modalInstance: NgbActiveModal,
private platform: PlatformService,
private serial: SerialService,
) {
}
) { }
getInputModeName (key) {
return this.inputModes.find(x => x.key === key)?.name
@@ -64,42 +61,34 @@ export class EditConnectionModalComponent {
}
async ngOnInit () {
this.connection.scripts = this.connection.scripts ?? []
this.profile.options.scripts = this.profile.options.scripts ?? []
this.foundPorts = await this.serial.listPorts()
}
save () {
this.modalInstance.close(this.connection)
}
cancel () {
this.modalInstance.dismiss()
}
moveScriptUp (script: LoginScript) {
if (!this.connection.scripts) {
this.connection.scripts = []
if (!this.profile.options.scripts) {
this.profile.options.scripts = []
}
const index = this.connection.scripts.indexOf(script)
const index = this.profile.options.scripts.indexOf(script)
if (index > 0) {
this.connection.scripts.splice(index, 1)
this.connection.scripts.splice(index - 1, 0, script)
this.profile.options.scripts.splice(index, 1)
this.profile.options.scripts.splice(index - 1, 0, script)
}
}
moveScriptDown (script: LoginScript) {
if (!this.connection.scripts) {
this.connection.scripts = []
if (!this.profile.options.scripts) {
this.profile.options.scripts = []
}
const index = this.connection.scripts.indexOf(script)
if (index >= 0 && index < this.connection.scripts.length - 1) {
this.connection.scripts.splice(index, 1)
this.connection.scripts.splice(index + 1, 0, script)
const index = this.profile.options.scripts.indexOf(script)
if (index >= 0 && index < this.profile.options.scripts.length - 1) {
this.profile.options.scripts.splice(index, 1)
this.profile.options.scripts.splice(index + 1, 0, script)
}
}
async deleteScript (script: LoginScript) {
if (this.connection.scripts && (await this.platform.showMessageBox(
if (this.profile.options.scripts && (await this.platform.showMessageBox(
{
type: 'warning',
message: 'Delete this script?',
@@ -108,14 +97,14 @@ export class EditConnectionModalComponent {
defaultId: 1,
}
)).response === 1) {
this.connection.scripts = this.connection.scripts.filter(x => x !== script)
this.profile.options.scripts = this.profile.options.scripts.filter(x => x !== script)
}
}
addScript () {
if (!this.connection.scripts) {
this.connection.scripts = []
if (!this.profile.options.scripts) {
this.profile.options.scripts = []
}
this.connection.scripts.push({ expect: '', send: '' })
this.profile.options.scripts.push({ expect: '', send: '' })
}
}

View File

@@ -1,16 +0,0 @@
h3 Connections
.list-group.list-group-flush.mt-3.mb-3
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let connection of connections',
(click)='editConnection(connection)'
)
.mr-auto
div {{connection.name}}
.text-muted {{connection.port}}
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)')
i.fas.fa-trash
button.btn.btn-primary((click)='createConnection()')
i.fas.fa-fw.fa-plus
span.ml-2 Add connection

View File

@@ -1,82 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, PlatformService } from 'tabby-core'
import { SerialConnection } from '../api'
import { EditConnectionModalComponent } from './editConnectionModal.component'
/** @hidden */
@Component({
template: require('./serialSettingsTab.component.pug'),
})
export class SerialSettingsTabComponent {
connections: SerialConnection[]
constructor (
public config: ConfigService,
private platform: PlatformService,
private ngbModal: NgbModal,
) {
this.connections = this.config.store.serial.connections
this.refresh()
}
createConnection () {
const connection: SerialConnection = {
name: '',
port: '',
baudrate: 115200,
databits: 8,
parity: 'none',
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
inputMode: null,
outputMode: null,
inputNewlines: null,
outputNewlines: null,
}
const modal = this.ngbModal.open(EditConnectionModalComponent)
modal.componentInstance.connection = connection
modal.result.then(result => {
this.connections.push(result)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
})
}
editConnection (connection: SerialConnection) {
const modal = this.ngbModal.open(EditConnectionModalComponent, { size: 'lg' })
modal.componentInstance.connection = Object.assign({}, connection)
modal.result.then(result => {
Object.assign(connection, result)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
})
}
async deleteConnection (connection: SerialConnection) {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete "${connection.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
this.connections = this.connections.filter(x => x !== connection)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
}
}
refresh () {
this.connections = this.config.store.serial.connections
}
}

View File

@@ -4,7 +4,7 @@
.toolbar
i.fas.fa-circle.text-success.mr-2(*ngIf='session && session.open')
i.fas.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open')
strong {{connection.port}} ({{connection.baudrate}})
strong {{profile.options.port}} ({{profile.options.baudrate}})
.mr-auto

View File

@@ -6,7 +6,7 @@ import { first } from 'rxjs/operators'
import { SelectorService } from 'tabby-core'
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { SerialService } from '../services/serial.service'
import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
import { SerialSession, BAUD_RATES, SerialProfile } from '../api'
/** @hidden */
@Component({
@@ -16,7 +16,7 @@ import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
animations: BaseTerminalTabComponent.animations,
})
export class SerialTabComponent extends BaseTerminalTabComponent {
connection?: SerialConnection
profile?: SerialProfile
session: SerialSession|null = null
serialPort: any
private serialService: SerialService
@@ -57,17 +57,17 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
super.ngOnInit()
setImmediate(() => {
this.setTitle(this.connection!.name)
this.setTitle(this.profile!.name)
})
}
async initializeSession () {
if (!this.connection) {
this.logger.error('No Serial connection info supplied')
if (!this.profile) {
this.logger.error('No serial profile info supplied')
return
}
const session = this.serialService.createSession(this.connection)
const session = this.serialService.createSession(this.profile)
this.setSession(session)
this.write(`Connecting to `)
@@ -112,7 +112,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
async getRecoveryToken (): Promise<any> {
return {
type: 'app:serial-tab',
connection: this.connection,
profile: this.profile,
savedState: this.frontend?.saveState(),
}
}
@@ -128,6 +128,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
name: x.toString(), result: x,
})))
this.serialPort.update({ baudRate: rate })
this.connection!.baudrate = rate
this.profile!.options.baudrate = rate
}
}

View File

@@ -3,11 +3,6 @@ import { ConfigProvider } from 'tabby-core'
/** @hidden */
export class SerialConfigProvider extends ConfigProvider {
defaults = {
serial: {
connections: [],
options: {
},
},
hotkeys: {
serial: [
'Alt-K',

View File

@@ -3,20 +3,16 @@ import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TabbyCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider, HotkeyProvider, CLIHandler } from 'tabby-core'
import { SettingsTabProvider } from 'tabby-settings'
import TabbyCoreModule, { ConfigProvider, TabRecoveryProvider, HotkeyProvider, ProfileProvider } from 'tabby-core'
import TabbyTerminalModule from 'tabby-terminal'
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
import { SerialSettingsTabComponent } from './components/serialSettingsTab.component'
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
import { SerialTabComponent } from './components/serialTab.component'
import { ButtonProvider } from './buttonProvider'
import { SerialConfigProvider } from './config'
import { SerialSettingsTabProvider } from './settings'
import { RecoveryProvider } from './recoveryProvider'
import { SerialHotkeyProvider } from './hotkeys'
import { SerialCLIHandler } from './cli'
import { SerialProfilesService } from './profiles'
/** @hidden */
@NgModule({
@@ -29,21 +25,17 @@ import { SerialCLIHandler } from './cli'
TabbyTerminalModule,
],
providers: [
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: ConfigProvider, useClass: SerialConfigProvider, multi: true },
{ provide: SettingsTabProvider, useClass: SerialSettingsTabProvider, multi: true },
{ provide: ProfileProvider, useClass: SerialProfilesService, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
{ provide: HotkeyProvider, useClass: SerialHotkeyProvider, multi: true },
{ provide: CLIHandler, useClass: SerialCLIHandler, multi: true },
],
entryComponents: [
EditConnectionModalComponent,
SerialSettingsTabComponent,
SerialProfileSettingsComponent,
SerialTabComponent,
],
declarations: [
EditConnectionModalComponent,
SerialSettingsTabComponent,
SerialProfileSettingsComponent,
SerialTabComponent,
],
})

View File

@@ -0,0 +1,74 @@
import slugify from 'slugify'
import deepClone from 'clone-deep'
import { Injectable } from '@angular/core'
import { ProfileProvider, NewTabParameters, SelectorService } from 'tabby-core'
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
import { SerialTabComponent } from './components/serialTab.component'
import { SerialService } from './services/serial.service'
import { BAUD_RATES, SerialProfile } from './api'
@Injectable({ providedIn: 'root' })
export class SerialProfilesService extends ProfileProvider {
id = 'serial'
name = 'Serial'
settingsComponent = SerialProfileSettingsComponent
constructor (
private selector: SelectorService,
private serial: SerialService,
) { super() }
async getBuiltinProfiles (): Promise<SerialProfile[]> {
return [
{
id: `serial:template`,
type: 'serial',
name: 'Serial connection',
icon: 'fas fa-microchip',
options: {
port: '',
databits: 8,
parity: 'none',
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
inputMode: null,
outputMode: null,
inputNewlines: null,
outputNewlines: null,
},
isBuiltin: true,
isTemplate: true,
},
...(await this.serial.listPorts()).map(p => ({
id: `serial:port-${slugify(p.name).replace('.', '-')}`,
type: 'serial',
name: p.description ? `Serial: ${p.description}` : 'Serial',
icon: 'fas fa-microchip',
isBuiltin: true,
options: {
port: p.name,
},
})),
]
}
async getNewTabParameters (profile: SerialProfile): Promise<NewTabParameters<SerialTabComponent>> {
if (!profile.options.baudrate) {
profile = deepClone(profile)
profile.options.baudrate = await this.selector.show('Baud rate', BAUD_RATES.map(x => ({
name: x.toString(), result: x,
})))
}
return {
type: SerialTabComponent,
inputs: { profile },
}
}
getDescription (profile: SerialProfile): string {
return profile.options.port
}
}

View File

@@ -1,20 +1,20 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from 'tabby-core'
import { TabRecoveryProvider, NewTabParameters, RecoveryToken } from 'tabby-core'
import { SerialTabComponent } from './components/serialTab.component'
/** @hidden */
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
export class RecoveryProvider extends TabRecoveryProvider<SerialTabComponent> {
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
return recoveryToken.type === 'app:serial-tab'
}
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
async recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<SerialTabComponent>> {
return {
type: SerialTabComponent,
options: {
connection: recoveryToken.connection,
inputs: {
profile: recoveryToken.profile,
savedState: recoveryToken.savedState,
},
}

View File

@@ -1,8 +1,7 @@
import { Injectable, NgZone } from '@angular/core'
import SerialPort from 'serialport'
import { LogService, AppService, SelectorOption, ConfigService, NotificationsService, SelectorService } from 'tabby-core'
import { SettingsTabComponent } from 'tabby-settings'
import { SerialConnection, SerialSession, SerialPortInfo, BAUD_RATES } from '../api'
import { LogService, NotificationsService, SelectorService, ProfilesService } from 'tabby-core'
import { SerialSession, SerialPortInfo, BAUD_RATES, SerialProfile } from '../api'
import { SerialTabComponent } from '../components/serialTab.component'
@Injectable({ providedIn: 'root' })
@@ -11,9 +10,8 @@ export class SerialService {
private log: LogService,
private zone: NgZone,
private notifications: NotificationsService,
private app: AppService,
private profilesService: ProfilesService,
private selector: SelectorService,
private config: ConfigService,
) { }
async listPorts (): Promise<SerialPortInfo[]> {
@@ -23,23 +21,23 @@ export class SerialService {
}))
}
createSession (connection: SerialConnection): SerialSession {
const session = new SerialSession(connection)
session.logger = this.log.create(`serial-${connection.port}`)
createSession (profile: SerialProfile): SerialSession {
const session = new SerialSession(profile)
session.logger = this.log.create(`serial-${profile.options.port}`)
return session
}
async connectSession (session: SerialSession): Promise<SerialPort> {
const serial = new SerialPort(session.connection.port, {
const serial = new SerialPort(session.profile.options.port, {
autoOpen: false,
baudRate: parseInt(session.connection.baudrate as any),
dataBits: session.connection.databits,
stopBits: session.connection.stopbits,
parity: session.connection.parity,
rtscts: session.connection.rtscts,
xon: session.connection.xon,
xoff: session.connection.xoff,
xany: session.connection.xany,
baudRate: parseInt(session.profile.options.baudrate as any),
dataBits: session.profile.options.databits,
stopBits: session.profile.options.stopbits,
parity: session.profile.options.parity,
rtscts: session.profile.options.rtscts,
xon: session.profile.options.xon,
xoff: session.profile.options.xoff,
xany: session.profile.options.xany,
})
session.serial = serial
let connected = false
@@ -72,105 +70,33 @@ export class SerialService {
return serial
}
async showConnectionSelector (): Promise<void> {
const options: SelectorOption<void>[] = []
const foundPorts = await this.listPorts()
try {
const lastConnection = JSON.parse(window.localStorage.lastSerialConnection)
if (lastConnection) {
options.push({
name: lastConnection.name,
icon: 'history',
callback: () => this.connect(lastConnection),
})
options.push({
name: 'Clear last connection',
icon: 'eraser',
callback: () => {
window.localStorage.lastSerialConnection = null
},
})
}
} catch { }
for (const port of foundPorts) {
options.push({
name: port.name,
description: port.description,
icon: 'arrow-right',
callback: () => this.connectFoundPort(port),
})
}
for (const connection of this.config.store.serial.connections) {
options.push({
name: connection.name,
description: connection.port,
callback: () => this.connect(connection),
})
}
options.push({
name: 'Manage connections',
icon: 'cog',
callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'serial' }),
})
options.push({
name: 'Quick connect',
freeInputPattern: 'Open device: %s...',
icon: 'arrow-right',
callback: query => this.quickConnect(query),
})
await this.selector.show('Open a serial port', options)
}
async connect (connection: SerialConnection): Promise<SerialTabComponent> {
try {
const tab = this.app.openNewTab(
SerialTabComponent,
{ connection }
) as SerialTabComponent
if (connection.color) {
(this.app.getParentTab(tab) ?? tab).color = connection.color
}
setTimeout(() => {
this.app.activeTab?.emitFocused()
})
return tab
} catch (error) {
this.notifications.error(`Could not connect: ${error}`)
throw error
}
}
quickConnect (query: string): Promise<SerialTabComponent> {
quickConnect (query: string): Promise<SerialTabComponent|null> {
let path = query
let baudrate = 115200
if (query.includes('@')) {
baudrate = parseInt(path.split('@')[1])
path = path.split('@')[0]
}
const connection: SerialConnection = {
const profile: SerialProfile = {
name: query,
port: path,
baudrate: baudrate,
databits: 8,
parity: 'none',
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
type: 'serial',
options: {
port: path,
baudrate: baudrate,
databits: 8,
parity: 'none',
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
},
}
window.localStorage.lastSerialConnection = JSON.stringify(connection)
return this.connect(connection)
window.localStorage.lastSerialConnection = JSON.stringify(profile)
return this.profilesService.openNewTabForProfile(profile) as Promise<SerialTabComponent|null>
}
async connectFoundPort (port: SerialPortInfo): Promise<SerialTabComponent> {
async connectFoundPort (port: SerialPortInfo): Promise<SerialTabComponent|null> {
const rate = await this.selector.show('Baud rate', BAUD_RATES.map(x => ({
name: x.toString(), result: x,
})))

View File

@@ -1,16 +0,0 @@
import { Injectable } from '@angular/core'
import { SettingsTabProvider } from 'tabby-settings'
import { SerialSettingsTabComponent } from './components/serialSettingsTab.component'
/** @hidden */
@Injectable()
export class SerialSettingsTabProvider extends SettingsTabProvider {
id = 'serial'
icon = 'keyboard'
title = 'Serial'
getComponentType (): any {
return SerialSettingsTabComponent
}
}