mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-19 18:07:58 +00:00
new profile system
This commit is contained in:
@@ -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[]
|
||||
}
|
||||
|
@@ -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()
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
171
tabby-serial/src/components/serialProfileSettings.component.pug
Normal file
171
tabby-serial/src/components/serialProfileSettings.component.pug
Normal 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')
|
@@ -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: '' })
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -3,11 +3,6 @@ import { ConfigProvider } from 'tabby-core'
|
||||
/** @hidden */
|
||||
export class SerialConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
serial: {
|
||||
connections: [],
|
||||
options: {
|
||||
},
|
||||
},
|
||||
hotkeys: {
|
||||
serial: [
|
||||
'Alt-K',
|
||||
|
@@ -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,
|
||||
],
|
||||
})
|
||||
|
74
tabby-serial/src/profiles.ts
Normal file
74
tabby-serial/src/profiles.ts
Normal 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
|
||||
}
|
||||
}
|
@@ -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,
|
||||
},
|
||||
}
|
||||
|
@@ -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,
|
||||
})))
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user