mirror of
https://github.com/Eugeny/tabby.git
synced 2024-11-22 03:26:09 +03:00
added a telnet client - fixes #760
This commit is contained in:
parent
59de67ca58
commit
827345d899
@ -5,28 +5,29 @@ const childProcess = require('child_process')
|
||||
|
||||
const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
|
||||
|
||||
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
|
||||
exports.version = childProcess.execSync('git describe --tags', { encoding:'utf-8' })
|
||||
exports.version = exports.version.substring(1).trim()
|
||||
exports.version = exports.version.replace('-', '-c')
|
||||
|
||||
if (exports.version.includes('-c')) {
|
||||
exports.version = semver.inc(exports.version, 'prepatch').replace('-0', '-nightly.0')
|
||||
exports.version = semver.inc(exports.version, 'prepatch').replace('-0', '-nightly.0')
|
||||
}
|
||||
|
||||
exports.builtinPlugins = [
|
||||
'tabby-core',
|
||||
'tabby-settings',
|
||||
'tabby-terminal',
|
||||
'tabby-electron',
|
||||
'tabby-local',
|
||||
'tabby-web',
|
||||
'tabby-community-color-schemes',
|
||||
'tabby-plugin-manager',
|
||||
'tabby-ssh',
|
||||
'tabby-serial',
|
||||
'tabby-core',
|
||||
'tabby-settings',
|
||||
'tabby-terminal',
|
||||
'tabby-electron',
|
||||
'tabby-local',
|
||||
'tabby-web',
|
||||
'tabby-community-color-schemes',
|
||||
'tabby-plugin-manager',
|
||||
'tabby-ssh',
|
||||
'tabby-serial',
|
||||
'tabby-telnet',
|
||||
]
|
||||
exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.electronVersion = electronInfo.version
|
||||
|
@ -14,6 +14,7 @@ export interface Profile {
|
||||
color?: string
|
||||
disableDynamicTitle?: boolean
|
||||
|
||||
weight?: number
|
||||
isBuiltin?: boolean
|
||||
isTemplate?: boolean
|
||||
}
|
||||
|
@ -19,18 +19,6 @@
|
||||
.description Toggles the Tabby window visibility
|
||||
toggle([(ngModel)]='enableGlobalHotkey')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable #[strong SSH] plugin
|
||||
.description Adds an SSH connection manager UI to Tabby
|
||||
toggle([(ngModel)]='enableSSH')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable #[strong Serial] plugin
|
||||
.description Allows attaching Tabby to serial ports
|
||||
toggle([(ngModel)]='enableSerial')
|
||||
|
||||
|
||||
.text-center.mt-5
|
||||
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again
|
||||
|
@ -11,8 +11,6 @@ import { HostWindowService } from '../api/hostWindow'
|
||||
styles: [require('./welcomeTab.component.scss')],
|
||||
})
|
||||
export class WelcomeTabComponent extends BaseTabComponent {
|
||||
enableSSH = false
|
||||
enableSerial = false
|
||||
enableGlobalHotkey = true
|
||||
|
||||
constructor (
|
||||
@ -21,19 +19,11 @@ export class WelcomeTabComponent extends BaseTabComponent {
|
||||
) {
|
||||
super()
|
||||
this.setTitle('Welcome')
|
||||
this.enableSSH = !config.store.pluginBlacklist.includes('ssh')
|
||||
this.enableSerial = !config.store.pluginBlacklist.includes('serial')
|
||||
}
|
||||
|
||||
closeAndDisable () {
|
||||
this.config.store.enableWelcomeTab = false
|
||||
this.config.store.pluginBlacklist = []
|
||||
if (!this.enableSSH) {
|
||||
this.config.store.pluginBlacklist.push('ssh')
|
||||
}
|
||||
if (!this.enableSerial) {
|
||||
this.config.store.pluginBlacklist.push('serial')
|
||||
}
|
||||
if (!this.enableGlobalHotkey) {
|
||||
this.config.store.hotkeys['toggle-window'] = []
|
||||
}
|
||||
|
@ -284,7 +284,7 @@ export class ConfigService {
|
||||
config.version = 2
|
||||
}
|
||||
if (config.version < 3) {
|
||||
delete config.ssh.recentConnections
|
||||
delete config.ssh?.recentConnections
|
||||
for (const c of config.ssh?.connections ?? []) {
|
||||
const p = {
|
||||
id: `ssh:${uuidv4()}`,
|
||||
|
@ -18,9 +18,9 @@ export class ProfilesService {
|
||||
if (params) {
|
||||
const tab = this.app.openNewTab(params)
|
||||
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
||||
tab.setTitle(profile.name)
|
||||
if (profile.disableDynamicTitle) {
|
||||
tab['enableDynamicTitle'] = false
|
||||
tab.setTitle(profile.name)
|
||||
}
|
||||
return tab
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
async newProfile (base?: Profile): Promise<void> {
|
||||
if (!base) {
|
||||
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
||||
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||
base = await this.selector.show(
|
||||
'Select a base profile to use as a template',
|
||||
profiles.map(p => ({
|
||||
@ -196,6 +197,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
return {
|
||||
ssh: 'secondary',
|
||||
serial: 'success',
|
||||
telnet: 'info',
|
||||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabby-serial",
|
||||
"version": "1.0.144",
|
||||
"description": "Serial connection manager for Tabby",
|
||||
"description": "Serial connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
],
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabby-ssh",
|
||||
"version": "1.0.144",
|
||||
"description": "SSH connection manager for Tabby",
|
||||
"description": "SSH connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
],
|
||||
|
@ -49,8 +49,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
this.logger = this.log.create('terminalTab')
|
||||
|
||||
this.enableDynamicTitle = !this.profile.disableDynamicTitle
|
||||
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
@ -82,10 +80,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
})
|
||||
|
||||
super.ngOnInit()
|
||||
|
||||
setImmediate(() => {
|
||||
this.setTitle(this.profile!.name)
|
||||
})
|
||||
}
|
||||
|
||||
async setupOneSession (session: SSHSession): Promise<void> {
|
||||
|
@ -10,10 +10,8 @@ export class SSHConfigProvider extends ConfigProvider {
|
||||
agentPath: null,
|
||||
},
|
||||
hotkeys: {
|
||||
ssh: [
|
||||
'Alt-S',
|
||||
],
|
||||
'restart-ssh-session': [],
|
||||
'launch-winscp': [],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ export class SSHProfilesService extends ProfileProvider {
|
||||
},
|
||||
isBuiltin: true,
|
||||
isTemplate: true,
|
||||
weight: -1,
|
||||
}]
|
||||
}
|
||||
|
||||
|
35
tabby-telnet/package.json
Normal file
35
tabby-telnet/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "tabby-telnet",
|
||||
"version": "1.0.144",
|
||||
"description": "Telnet/socket connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "typings/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --color",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"files": [
|
||||
"typings"
|
||||
],
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "14.14.31",
|
||||
"cli-spinner": "^0.2.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^9.1.9",
|
||||
"@angular/common": "^9.1.11",
|
||||
"@angular/core": "^9.1.9",
|
||||
"@angular/forms": "^9.1.11",
|
||||
"@angular/platform-browser": "^9.1.11",
|
||||
"@ng-bootstrap/ng-bootstrap": "^6.1.0",
|
||||
"rxjs": "^6.5.5",
|
||||
"tabby-core": "*",
|
||||
"tabby-settings": "*",
|
||||
"tabby-terminal": "*"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
.form-group
|
||||
label Host
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='profile.options.host',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Port
|
||||
input.form-control(
|
||||
type='number',
|
||||
placeholder='22',
|
||||
[(ngModel)]='profile.options.port',
|
||||
)
|
||||
|
||||
stream-processing-settings([options]='profile.options')
|
@ -0,0 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
import { ProfileSettingsComponent } from 'tabby-core'
|
||||
import { TelnetProfile } from '../session'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./telnetProfileSettings.component.pug'),
|
||||
})
|
||||
export class TelnetProfileSettingsComponent implements ProfileSettingsComponent {
|
||||
profile: TelnetProfile
|
||||
}
|
10
tabby-telnet/src/components/telnetTab.component.pug
Normal file
10
tabby-telnet/src/components/telnetTab.component.pug
Normal file
@ -0,0 +1,10 @@
|
||||
.tab-toolbar([class.show]='!session || !session.open')
|
||||
.btn.btn-outline-secondary.reveal-button
|
||||
i.fas.fa-ellipsis-h
|
||||
.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.mr-auto {{profile.options.host}}:{{profile.options.port}}
|
||||
|
||||
button.btn.btn-secondary.mr-2((click)='reconnect()', [class.btn-info]='!session || !session.open')
|
||||
span Reconnect
|
1
tabby-telnet/src/components/telnetTab.component.scss
Normal file
1
tabby-telnet/src/components/telnetTab.component.scss
Normal file
@ -0,0 +1 @@
|
||||
@import '../../../tabby-ssh/src/components/sshTab.component.scss';
|
156
tabby-telnet/src/components/telnetTab.component.ts
Normal file
156
tabby-telnet/src/components/telnetTab.component.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { Spinner } from 'cli-spinner'
|
||||
import { Component, Injector } from '@angular/core'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { Platform, RecoveryToken } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { TelnetProfile, TelnetSession } from '../session'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'telnet-tab',
|
||||
template: `${BaseTerminalTabComponent.template} ${require('./telnetTab.component.pug')}`,
|
||||
styles: [require('./telnetTab.component.scss'), ...BaseTerminalTabComponent.styles],
|
||||
animations: BaseTerminalTabComponent.animations,
|
||||
})
|
||||
export class TelnetTabComponent extends BaseTerminalTabComponent {
|
||||
Platform = Platform
|
||||
profile?: TelnetProfile
|
||||
session: TelnetSession|null = null
|
||||
private reconnectOffered = false
|
||||
private spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
stream: {
|
||||
write: x => this.write(x),
|
||||
},
|
||||
})
|
||||
private spinnerActive = false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
injector: Injector,
|
||||
) {
|
||||
super(injector)
|
||||
}
|
||||
|
||||
ngOnInit (): void {
|
||||
if (!this.profile) {
|
||||
throw new Error('Profile not set')
|
||||
}
|
||||
|
||||
this.logger = this.log.create('telnetTab')
|
||||
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (this.hasFocus && hotkey === 'restart-telnet-session') {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
|
||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||
this.initializeSession()
|
||||
})
|
||||
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
protected attachSessionHandlers (): void {
|
||||
const session = this.session!
|
||||
this.attachSessionHandler(session.destroyed$, () => {
|
||||
if (this.frontend) {
|
||||
// Session was closed abruptly
|
||||
if (!this.reconnectOffered) {
|
||||
this.reconnectOffered = true
|
||||
this.write('Press any key to reconnect\r\n')
|
||||
this.input$.pipe(first()).subscribe(() => {
|
||||
if (!this.session?.open && this.reconnectOffered) {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
super.attachSessionHandlers()
|
||||
}
|
||||
|
||||
async initializeSession (): Promise<void> {
|
||||
this.reconnectOffered = false
|
||||
if (!this.profile) {
|
||||
this.logger.error('No Telnet connection info supplied')
|
||||
return
|
||||
}
|
||||
|
||||
const session = new TelnetSession(this.injector, this.profile)
|
||||
this.setSession(session)
|
||||
|
||||
try {
|
||||
this.startSpinner()
|
||||
|
||||
this.attachSessionHandler(session.serviceMessage$, msg => {
|
||||
this.pauseSpinner(() => {
|
||||
this.write(`\r${colors.black.bgWhite(' Telnet ')} ${msg}\r\n`)
|
||||
session.resize(this.size.columns, this.size.rows)
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
await session.start()
|
||||
this.stopSpinner()
|
||||
} catch (e) {
|
||||
this.stopSpinner()
|
||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<RecoveryToken> {
|
||||
return {
|
||||
type: 'app:telnet-tab',
|
||||
profile: this.profile,
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect (): Promise<void> {
|
||||
this.session?.destroy()
|
||||
await this.initializeSession()
|
||||
this.session?.releaseInitialDataBuffer()
|
||||
}
|
||||
|
||||
async canClose (): Promise<boolean> {
|
||||
if (!this.session?.open) {
|
||||
return true
|
||||
}
|
||||
return (await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Disconnect from ${this.profile?.options.host}?`,
|
||||
buttons: ['Cancel', 'Disconnect'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1
|
||||
}
|
||||
|
||||
private startSpinner () {
|
||||
this.spinner.setSpinnerString(6)
|
||||
this.spinner.start()
|
||||
this.spinnerActive = true
|
||||
}
|
||||
|
||||
private stopSpinner () {
|
||||
this.spinner.stop(true)
|
||||
this.spinnerActive = false
|
||||
}
|
||||
|
||||
private pauseSpinner (work: () => void) {
|
||||
const wasActive = this.spinnerActive
|
||||
this.stopSpinner()
|
||||
work()
|
||||
if (wasActive) {
|
||||
this.startSpinner()
|
||||
}
|
||||
}
|
||||
}
|
12
tabby-telnet/src/config.ts
Normal file
12
tabby-telnet/src/config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ConfigProvider } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
export class TelnetConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
hotkeys: {
|
||||
'restart-telnet-session': [],
|
||||
},
|
||||
}
|
||||
|
||||
platformDefaults = { }
|
||||
}
|
17
tabby-telnet/src/hotkeys.ts
Normal file
17
tabby-telnet/src/hotkeys.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HotkeyDescription, HotkeyProvider } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class TelnetHotkeyProvider extends HotkeyProvider {
|
||||
hotkeys: HotkeyDescription[] = [
|
||||
{
|
||||
id: 'restart-telnet-session',
|
||||
name: 'Restart current Telnet session',
|
||||
},
|
||||
]
|
||||
|
||||
async provide (): Promise<HotkeyDescription[]> {
|
||||
return this.hotkeys
|
||||
}
|
||||
}
|
44
tabby-telnet/src/index.ts
Normal file
44
tabby-telnet/src/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
import TabbyCoreModule, { ConfigProvider, TabRecoveryProvider, HotkeyProvider, ProfileProvider } from 'tabby-core'
|
||||
import TabbyTerminalModule from 'tabby-terminal'
|
||||
|
||||
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
|
||||
import { TelnetTabComponent } from './components/telnetTab.component'
|
||||
|
||||
import { TelnetConfigProvider } from './config'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
import { TelnetHotkeyProvider } from './hotkeys'
|
||||
import { TelnetProfilesService } from './profiles'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
imports: [
|
||||
NgbModule,
|
||||
NgxFilesizeModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ToastrModule,
|
||||
TabbyCoreModule,
|
||||
TabbyTerminalModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ConfigProvider, useClass: TelnetConfigProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
{ provide: HotkeyProvider, useClass: TelnetHotkeyProvider, multi: true },
|
||||
{ provide: ProfileProvider, useClass: TelnetProfilesService, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
TelnetProfileSettingsComponent,
|
||||
TelnetTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
TelnetProfileSettingsComponent,
|
||||
TelnetTabComponent,
|
||||
],
|
||||
})
|
||||
export default class TelnetModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
71
tabby-telnet/src/profiles.ts
Normal file
71
tabby-telnet/src/profiles.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfileProvider, Profile, NewTabParameters } from 'tabby-core'
|
||||
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
|
||||
import { TelnetTabComponent } from './components/telnetTab.component'
|
||||
import { TelnetProfile } from './session'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TelnetProfilesService extends ProfileProvider {
|
||||
id = 'telnet'
|
||||
name = 'Telnet'
|
||||
supportsQuickConnect = true
|
||||
settingsComponent = TelnetProfileSettingsComponent
|
||||
|
||||
async getBuiltinProfiles (): Promise<TelnetProfile[]> {
|
||||
return [{
|
||||
id: `telnet:template`,
|
||||
type: 'telnet',
|
||||
name: 'Telnet/socket connection',
|
||||
icon: 'fas fa-network-wired',
|
||||
options: {
|
||||
host: '',
|
||||
port: 23,
|
||||
inputMode: 'local-echo',
|
||||
outputMode: null,
|
||||
inputNewlines: null,
|
||||
outputNewlines: 'crlf',
|
||||
},
|
||||
isBuiltin: true,
|
||||
isTemplate: true,
|
||||
}]
|
||||
}
|
||||
|
||||
async getNewTabParameters (profile: Profile): Promise<NewTabParameters<TelnetTabComponent>> {
|
||||
return {
|
||||
type: TelnetTabComponent,
|
||||
inputs: { profile },
|
||||
}
|
||||
}
|
||||
|
||||
getDescription (profile: TelnetProfile): string {
|
||||
return profile.options.host ? `${profile.options.host}:${profile.options.port}` : ''
|
||||
}
|
||||
|
||||
quickConnect (query: string): TelnetProfile|null {
|
||||
if (!query.startsWith('telnet:')) {
|
||||
return null
|
||||
}
|
||||
query = query.substring('telnet:'.length)
|
||||
|
||||
let host = query
|
||||
let port = 23
|
||||
if (host.includes('[')) {
|
||||
port = parseInt(host.split(']')[1].substring(1))
|
||||
host = host.split(']')[0].substring(1)
|
||||
} else if (host.includes(':')) {
|
||||
port = parseInt(host.split(/:/g)[1])
|
||||
host = host.split(':')[0]
|
||||
}
|
||||
|
||||
return {
|
||||
name: query,
|
||||
type: 'telnet',
|
||||
options: {
|
||||
host,
|
||||
port,
|
||||
inputMode: 'local-echo',
|
||||
outputNewlines: 'crlf',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
29
tabby-telnet/src/recoveryProvider.ts
Normal file
29
tabby-telnet/src/recoveryProvider.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, NewTabParameters, RecoveryToken } from 'tabby-core'
|
||||
|
||||
import { TelnetTabComponent } from './components/telnetTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider<TelnetTabComponent> {
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:telnet-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<TelnetTabComponent>> {
|
||||
return {
|
||||
type: TelnetTabComponent,
|
||||
inputs: {
|
||||
profile: recoveryToken['profile'],
|
||||
savedState: recoveryToken['savedState'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
duplicate (recoveryToken: RecoveryToken): RecoveryToken {
|
||||
return {
|
||||
...recoveryToken,
|
||||
savedState: null,
|
||||
}
|
||||
}
|
||||
}
|
102
tabby-telnet/src/session.ts
Normal file
102
tabby-telnet/src/session.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Socket } from 'net'
|
||||
import colors from 'ansi-colors'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { Injector } from '@angular/core'
|
||||
import { Logger, Profile, LogService } from 'tabby-core'
|
||||
import { BaseSession, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
|
||||
|
||||
export interface TelnetProfile extends Profile {
|
||||
options: TelnetProfileOptions
|
||||
}
|
||||
|
||||
export interface TelnetProfileOptions extends StreamProcessingOptions {
|
||||
host: string
|
||||
port?: number
|
||||
}
|
||||
|
||||
export class TelnetSession extends BaseSession {
|
||||
logger: Logger
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
|
||||
private serviceMessage = new Subject<string>()
|
||||
private socket: Socket
|
||||
private streamProcessor: TerminalStreamProcessor
|
||||
|
||||
constructor (
|
||||
injector: Injector,
|
||||
public profile: TelnetProfile,
|
||||
) {
|
||||
super()
|
||||
this.logger = injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`)
|
||||
this.streamProcessor = new TerminalStreamProcessor(profile.options)
|
||||
this.streamProcessor.outputToSession$.subscribe(data => {
|
||||
this.socket.write(data)
|
||||
})
|
||||
this.streamProcessor.outputToTerminal$.subscribe(data => {
|
||||
this.emitOutput(data)
|
||||
})
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.socket = new Socket()
|
||||
this.emitServiceMessage(`Connecting to ${this.profile.options.host}`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket.on('error', err => {
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Socket error: ${err as any}`)
|
||||
reject()
|
||||
this.destroy()
|
||||
})
|
||||
this.socket.on('close', () => {
|
||||
this.emitServiceMessage('Connection closed')
|
||||
this.destroy()
|
||||
})
|
||||
this.socket.on('data', data => this.streamProcessor.feedFromSession(data))
|
||||
this.socket.connect(this.profile.options.port ?? 23, this.profile.options.host, () => {
|
||||
this.emitServiceMessage('Connected')
|
||||
this.open = true
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emitServiceMessage (msg: string): void {
|
||||
this.serviceMessage.next(msg)
|
||||
this.logger.info(stripAnsi(msg))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
resize (_w: number, _h: number): void { }
|
||||
|
||||
write (data: Buffer): void {
|
||||
this.streamProcessor.feedFromTerminal(data)
|
||||
}
|
||||
|
||||
kill (_signal?: string): void {
|
||||
this.socket.destroy()
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
this.serviceMessage.complete()
|
||||
this.kill()
|
||||
await super.destroy()
|
||||
}
|
||||
|
||||
async getChildProcesses (): Promise<any[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
async gracefullyKillProcess (): Promise<void> {
|
||||
this.kill()
|
||||
}
|
||||
|
||||
supportsWorkingDirectory (): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string|null> {
|
||||
return null
|
||||
}
|
||||
}
|
7
tabby-telnet/tsconfig.json
Normal file
7
tabby-telnet/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "dist", "typings"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
15
tabby-telnet/tsconfig.typings.json
Normal file
15
tabby-telnet/tsconfig.typings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "dist", "typings"],
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "./typings",
|
||||
"paths": {
|
||||
"tabby-*": ["../../tabby-*"],
|
||||
"*": ["../../app/node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
5
tabby-telnet/webpack.config.js
Normal file
5
tabby-telnet/webpack.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
const config = require('../webpack.plugin.config')
|
||||
module.exports = config({
|
||||
name: 'telnet',
|
||||
dirname: __dirname
|
||||
})
|
13
tabby-telnet/yarn.lock
Normal file
13
tabby-telnet/yarn.lock
Normal file
@ -0,0 +1,13 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@14.14.31":
|
||||
version "14.14.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
|
||||
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
|
||||
|
||||
cli-spinner@^0.2.10:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"
|
||||
integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==
|
@ -7,7 +7,7 @@ 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 InputMode = null | 'local-echo' | '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
|
||||
|
||||
@ -76,6 +76,9 @@ export class TerminalStreamProcessor {
|
||||
}
|
||||
|
||||
feedFromTerminal (data: Buffer): void {
|
||||
if (this.options.inputMode === 'local-echo') {
|
||||
this.outputToTerminal.next(this.replaceNewlines(data, 'crlf'))
|
||||
}
|
||||
if (this.options.inputMode?.startsWith('readline')) {
|
||||
this.inputReadlineInStream.write(data)
|
||||
} else {
|
||||
|
@ -12,6 +12,7 @@ export class StreamProcessingSettingsComponent {
|
||||
|
||||
inputModes = [
|
||||
{ key: null, name: 'Normal', description: 'Input is sent as you type' },
|
||||
{ key: 'local-echo', name: 'Local echo', description: 'Immediately echoes your input locally' },
|
||||
{ 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' },
|
||||
]
|
||||
|
@ -42,12 +42,12 @@ export abstract class BaseSession {
|
||||
this.open = false
|
||||
this.closed.next()
|
||||
this.destroyed.next()
|
||||
this.closed.complete()
|
||||
this.destroyed.complete()
|
||||
this.output.complete()
|
||||
this.binaryOutput.complete()
|
||||
await this.gracefullyKillProcess()
|
||||
}
|
||||
this.closed.complete()
|
||||
this.destroyed.complete()
|
||||
this.output.complete()
|
||||
this.binaryOutput.complete()
|
||||
}
|
||||
|
||||
abstract start (options: unknown): void
|
||||
|
@ -91,7 +91,6 @@ export class WebPlatformService extends PlatformService {
|
||||
}
|
||||
|
||||
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> {
|
||||
console.log(options)
|
||||
const modal = this.ngbModal.open(MessageBoxModalComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
|
@ -1,16 +1,13 @@
|
||||
module.exports = [
|
||||
require('./app/webpack.config.js'),
|
||||
require('./app/webpack.main.config.js'),
|
||||
require('./tabby-core/webpack.config.js'),
|
||||
require('./tabby-electron/webpack.config.js'),
|
||||
require('./tabby-web/webpack.config.js'),
|
||||
require('./tabby-settings/webpack.config.js'),
|
||||
require('./tabby-terminal/webpack.config.js'),
|
||||
require('./tabby-local/webpack.config.js'),
|
||||
require('./tabby-community-color-schemes/webpack.config.js'),
|
||||
require('./tabby-plugin-manager/webpack.config.js'),
|
||||
require('./tabby-ssh/webpack.config.js'),
|
||||
require('./tabby-serial/webpack.config.js'),
|
||||
require('./tabby-web/webpack.config.js'),
|
||||
require('./web/webpack.config.js'),
|
||||
const log = require('npmlog')
|
||||
const { builtinPlugins } = require('./scripts/vars')
|
||||
|
||||
const paths = [
|
||||
'./app/webpack.config.js',
|
||||
'./app/webpack.main.config.js',
|
||||
'./web/webpack.config.js',
|
||||
...builtinPlugins.map(x => `./${x}/webpack.config.js`),
|
||||
]
|
||||
|
||||
paths.forEach(x => log.info(`Using config: ${x}`))
|
||||
|
||||
module.exports = paths.map(x => require(x))
|
||||
|
Loading…
Reference in New Issue
Block a user