mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-08 21:40:03 +00: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')))
|
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.substring(1).trim()
|
||||||
exports.version = exports.version.replace('-', '-c')
|
exports.version = exports.version.replace('-', '-c')
|
||||||
|
|
||||||
if (exports.version.includes('-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 = [
|
exports.builtinPlugins = [
|
||||||
'tabby-core',
|
'tabby-core',
|
||||||
'tabby-settings',
|
'tabby-settings',
|
||||||
'tabby-terminal',
|
'tabby-terminal',
|
||||||
'tabby-electron',
|
'tabby-electron',
|
||||||
'tabby-local',
|
'tabby-local',
|
||||||
'tabby-web',
|
'tabby-web',
|
||||||
'tabby-community-color-schemes',
|
'tabby-community-color-schemes',
|
||||||
'tabby-plugin-manager',
|
'tabby-plugin-manager',
|
||||||
'tabby-ssh',
|
'tabby-ssh',
|
||||||
'tabby-serial',
|
'tabby-serial',
|
||||||
|
'tabby-telnet',
|
||||||
]
|
]
|
||||||
exports.bundledModules = [
|
exports.bundledModules = [
|
||||||
'@angular',
|
'@angular',
|
||||||
'@ng-bootstrap',
|
'@ng-bootstrap',
|
||||||
]
|
]
|
||||||
exports.electronVersion = electronInfo.version
|
exports.electronVersion = electronInfo.version
|
||||||
|
@ -14,6 +14,7 @@ export interface Profile {
|
|||||||
color?: string
|
color?: string
|
||||||
disableDynamicTitle?: boolean
|
disableDynamicTitle?: boolean
|
||||||
|
|
||||||
|
weight?: number
|
||||||
isBuiltin?: boolean
|
isBuiltin?: boolean
|
||||||
isTemplate?: boolean
|
isTemplate?: boolean
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,6 @@
|
|||||||
.description Toggles the Tabby window visibility
|
.description Toggles the Tabby window visibility
|
||||||
toggle([(ngModel)]='enableGlobalHotkey')
|
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
|
.text-center.mt-5
|
||||||
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again
|
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')],
|
styles: [require('./welcomeTab.component.scss')],
|
||||||
})
|
})
|
||||||
export class WelcomeTabComponent extends BaseTabComponent {
|
export class WelcomeTabComponent extends BaseTabComponent {
|
||||||
enableSSH = false
|
|
||||||
enableSerial = false
|
|
||||||
enableGlobalHotkey = true
|
enableGlobalHotkey = true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@ -21,19 +19,11 @@ export class WelcomeTabComponent extends BaseTabComponent {
|
|||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.setTitle('Welcome')
|
this.setTitle('Welcome')
|
||||||
this.enableSSH = !config.store.pluginBlacklist.includes('ssh')
|
|
||||||
this.enableSerial = !config.store.pluginBlacklist.includes('serial')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAndDisable () {
|
closeAndDisable () {
|
||||||
this.config.store.enableWelcomeTab = false
|
this.config.store.enableWelcomeTab = false
|
||||||
this.config.store.pluginBlacklist = []
|
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) {
|
if (!this.enableGlobalHotkey) {
|
||||||
this.config.store.hotkeys['toggle-window'] = []
|
this.config.store.hotkeys['toggle-window'] = []
|
||||||
}
|
}
|
||||||
|
@ -284,7 +284,7 @@ export class ConfigService {
|
|||||||
config.version = 2
|
config.version = 2
|
||||||
}
|
}
|
||||||
if (config.version < 3) {
|
if (config.version < 3) {
|
||||||
delete config.ssh.recentConnections
|
delete config.ssh?.recentConnections
|
||||||
for (const c of config.ssh?.connections ?? []) {
|
for (const c of config.ssh?.connections ?? []) {
|
||||||
const p = {
|
const p = {
|
||||||
id: `ssh:${uuidv4()}`,
|
id: `ssh:${uuidv4()}`,
|
||||||
|
@ -18,9 +18,9 @@ export class ProfilesService {
|
|||||||
if (params) {
|
if (params) {
|
||||||
const tab = this.app.openNewTab(params)
|
const tab = this.app.openNewTab(params)
|
||||||
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
||||||
|
tab.setTitle(profile.name)
|
||||||
if (profile.disableDynamicTitle) {
|
if (profile.disableDynamicTitle) {
|
||||||
tab['enableDynamicTitle'] = false
|
tab['enableDynamicTitle'] = false
|
||||||
tab.setTitle(profile.name)
|
|
||||||
}
|
}
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||||||
async newProfile (base?: Profile): Promise<void> {
|
async newProfile (base?: Profile): Promise<void> {
|
||||||
if (!base) {
|
if (!base) {
|
||||||
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
||||||
|
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||||
base = await this.selector.show(
|
base = await this.selector.show(
|
||||||
'Select a base profile to use as a template',
|
'Select a base profile to use as a template',
|
||||||
profiles.map(p => ({
|
profiles.map(p => ({
|
||||||
@ -196,6 +197,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||||||
return {
|
return {
|
||||||
ssh: 'secondary',
|
ssh: 'secondary',
|
||||||
serial: 'success',
|
serial: 'success',
|
||||||
|
telnet: 'info',
|
||||||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-serial",
|
"name": "tabby-serial",
|
||||||
"version": "1.0.144",
|
"version": "1.0.144",
|
||||||
"description": "Serial connection manager for Tabby",
|
"description": "Serial connections for Tabby",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
],
|
],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-ssh",
|
"name": "tabby-ssh",
|
||||||
"version": "1.0.144",
|
"version": "1.0.144",
|
||||||
"description": "SSH connection manager for Tabby",
|
"description": "SSH connections for Tabby",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
],
|
],
|
||||||
|
@ -49,8 +49,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
|
|
||||||
this.logger = this.log.create('terminalTab')
|
this.logger = this.log.create('terminalTab')
|
||||||
|
|
||||||
this.enableDynamicTitle = !this.profile.disableDynamicTitle
|
|
||||||
|
|
||||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||||
if (!this.hasFocus) {
|
if (!this.hasFocus) {
|
||||||
return
|
return
|
||||||
@ -82,10 +80,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
this.setTitle(this.profile!.name)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupOneSession (session: SSHSession): Promise<void> {
|
async setupOneSession (session: SSHSession): Promise<void> {
|
||||||
|
@ -10,10 +10,8 @@ export class SSHConfigProvider extends ConfigProvider {
|
|||||||
agentPath: null,
|
agentPath: null,
|
||||||
},
|
},
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
ssh: [
|
|
||||||
'Alt-S',
|
|
||||||
],
|
|
||||||
'restart-ssh-session': [],
|
'restart-ssh-session': [],
|
||||||
|
'launch-winscp': [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ export class SSHProfilesService extends ProfileProvider {
|
|||||||
},
|
},
|
||||||
isBuiltin: true,
|
isBuiltin: true,
|
||||||
isTemplate: 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 { PassThrough, Readable, Writable } from 'stream'
|
||||||
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
|
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 OutputMode = null | 'hex' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||||
export type NewlineMode = null | 'cr' | 'lf' | 'crlf' // eslint-disable-line @typescript-eslint/no-type-alias
|
export 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 {
|
feedFromTerminal (data: Buffer): void {
|
||||||
|
if (this.options.inputMode === 'local-echo') {
|
||||||
|
this.outputToTerminal.next(this.replaceNewlines(data, 'crlf'))
|
||||||
|
}
|
||||||
if (this.options.inputMode?.startsWith('readline')) {
|
if (this.options.inputMode?.startsWith('readline')) {
|
||||||
this.inputReadlineInStream.write(data)
|
this.inputReadlineInStream.write(data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -12,6 +12,7 @@ export class StreamProcessingSettingsComponent {
|
|||||||
|
|
||||||
inputModes = [
|
inputModes = [
|
||||||
{ key: null, name: 'Normal', description: 'Input is sent as you type' },
|
{ 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', 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' },
|
{ 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.open = false
|
||||||
this.closed.next()
|
this.closed.next()
|
||||||
this.destroyed.next()
|
this.destroyed.next()
|
||||||
this.closed.complete()
|
|
||||||
this.destroyed.complete()
|
|
||||||
this.output.complete()
|
|
||||||
this.binaryOutput.complete()
|
|
||||||
await this.gracefullyKillProcess()
|
await this.gracefullyKillProcess()
|
||||||
}
|
}
|
||||||
|
this.closed.complete()
|
||||||
|
this.destroyed.complete()
|
||||||
|
this.output.complete()
|
||||||
|
this.binaryOutput.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract start (options: unknown): void
|
abstract start (options: unknown): void
|
||||||
|
@ -91,7 +91,6 @@ export class WebPlatformService extends PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> {
|
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> {
|
||||||
console.log(options)
|
|
||||||
const modal = this.ngbModal.open(MessageBoxModalComponent, {
|
const modal = this.ngbModal.open(MessageBoxModalComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
module.exports = [
|
const log = require('npmlog')
|
||||||
require('./app/webpack.config.js'),
|
const { builtinPlugins } = require('./scripts/vars')
|
||||||
require('./app/webpack.main.config.js'),
|
|
||||||
require('./tabby-core/webpack.config.js'),
|
const paths = [
|
||||||
require('./tabby-electron/webpack.config.js'),
|
'./app/webpack.config.js',
|
||||||
require('./tabby-web/webpack.config.js'),
|
'./app/webpack.main.config.js',
|
||||||
require('./tabby-settings/webpack.config.js'),
|
'./web/webpack.config.js',
|
||||||
require('./tabby-terminal/webpack.config.js'),
|
...builtinPlugins.map(x => `./${x}/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'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
paths.forEach(x => log.info(`Using config: ${x}`))
|
||||||
|
|
||||||
|
module.exports = paths.map(x => require(x))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user