diff --git a/scripts/vars.js b/scripts/vars.js index b2855914..6060fe79 100755 --- a/scripts/vars.js +++ b/scripts/vars.js @@ -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 diff --git a/tabby-core/src/api/profileProvider.ts b/tabby-core/src/api/profileProvider.ts index 3f0c2665..6bb1b257 100644 --- a/tabby-core/src/api/profileProvider.ts +++ b/tabby-core/src/api/profileProvider.ts @@ -14,6 +14,7 @@ export interface Profile { color?: string disableDynamicTitle?: boolean + weight?: number isBuiltin?: boolean isTemplate?: boolean } diff --git a/tabby-core/src/components/welcomeTab.component.pug b/tabby-core/src/components/welcomeTab.component.pug index 8bfc382b..719824ae 100644 --- a/tabby-core/src/components/welcomeTab.component.pug +++ b/tabby-core/src/components/welcomeTab.component.pug @@ -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 diff --git a/tabby-core/src/components/welcomeTab.component.ts b/tabby-core/src/components/welcomeTab.component.ts index 025c7574..d0fac0c9 100644 --- a/tabby-core/src/components/welcomeTab.component.ts +++ b/tabby-core/src/components/welcomeTab.component.ts @@ -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'] = [] } diff --git a/tabby-core/src/services/config.service.ts b/tabby-core/src/services/config.service.ts index deb6fe46..fcd9d3a1 100644 --- a/tabby-core/src/services/config.service.ts +++ b/tabby-core/src/services/config.service.ts @@ -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()}`, diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index 2a3fa869..4a4230cc 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/tabby-core/src/services/profiles.service.ts @@ -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 } diff --git a/tabby-local/src/components/profilesSettingsTab.component.ts b/tabby-local/src/components/profilesSettingsTab.component.ts index 549858ce..ad5bb942 100644 --- a/tabby-local/src/components/profilesSettingsTab.component.ts +++ b/tabby-local/src/components/profilesSettingsTab.component.ts @@ -51,6 +51,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent { async newProfile (base?: Profile): Promise { 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' } } diff --git a/tabby-serial/package.json b/tabby-serial/package.json index 219e25c0..62014bf1 100644 --- a/tabby-serial/package.json +++ b/tabby-serial/package.json @@ -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" ], diff --git a/tabby-ssh/package.json b/tabby-ssh/package.json index 7c40834e..2ba320e9 100644 --- a/tabby-ssh/package.json +++ b/tabby-ssh/package.json @@ -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" ], diff --git a/tabby-ssh/src/components/sshTab.component.ts b/tabby-ssh/src/components/sshTab.component.ts index 42c5f500..9c638bf3 100644 --- a/tabby-ssh/src/components/sshTab.component.ts +++ b/tabby-ssh/src/components/sshTab.component.ts @@ -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 { diff --git a/tabby-ssh/src/config.ts b/tabby-ssh/src/config.ts index 5b9f41dc..eea5b1e9 100644 --- a/tabby-ssh/src/config.ts +++ b/tabby-ssh/src/config.ts @@ -10,10 +10,8 @@ export class SSHConfigProvider extends ConfigProvider { agentPath: null, }, hotkeys: { - ssh: [ - 'Alt-S', - ], 'restart-ssh-session': [], + 'launch-winscp': [], }, } diff --git a/tabby-ssh/src/profiles.ts b/tabby-ssh/src/profiles.ts index 1bfc5587..eba437bb 100644 --- a/tabby-ssh/src/profiles.ts +++ b/tabby-ssh/src/profiles.ts @@ -31,6 +31,7 @@ export class SSHProfilesService extends ProfileProvider { }, isBuiltin: true, isTemplate: true, + weight: -1, }] } diff --git a/tabby-telnet/package.json b/tabby-telnet/package.json new file mode 100644 index 00000000..b623c903 --- /dev/null +++ b/tabby-telnet/package.json @@ -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": "*" + } +} diff --git a/tabby-telnet/src/components/telnetProfileSettings.component.pug b/tabby-telnet/src/components/telnetProfileSettings.component.pug new file mode 100644 index 00000000..cce09398 --- /dev/null +++ b/tabby-telnet/src/components/telnetProfileSettings.component.pug @@ -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') diff --git a/tabby-telnet/src/components/telnetProfileSettings.component.ts b/tabby-telnet/src/components/telnetProfileSettings.component.ts new file mode 100644 index 00000000..0b6c21c4 --- /dev/null +++ b/tabby-telnet/src/components/telnetProfileSettings.component.ts @@ -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 +} diff --git a/tabby-telnet/src/components/telnetTab.component.pug b/tabby-telnet/src/components/telnetTab.component.pug new file mode 100644 index 00000000..2aec95c6 --- /dev/null +++ b/tabby-telnet/src/components/telnetTab.component.pug @@ -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 diff --git a/tabby-telnet/src/components/telnetTab.component.scss b/tabby-telnet/src/components/telnetTab.component.scss new file mode 100644 index 00000000..d33f54e7 --- /dev/null +++ b/tabby-telnet/src/components/telnetTab.component.scss @@ -0,0 +1 @@ +@import '../../../tabby-ssh/src/components/sshTab.component.scss'; diff --git a/tabby-telnet/src/components/telnetTab.component.ts b/tabby-telnet/src/components/telnetTab.component.ts new file mode 100644 index 00000000..9938dac2 --- /dev/null +++ b/tabby-telnet/src/components/telnetTab.component.ts @@ -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 { + 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 { + return { + type: 'app:telnet-tab', + profile: this.profile, + savedState: this.frontend?.saveState(), + } + } + + async reconnect (): Promise { + this.session?.destroy() + await this.initializeSession() + this.session?.releaseInitialDataBuffer() + } + + async canClose (): Promise { + 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() + } + } +} diff --git a/tabby-telnet/src/config.ts b/tabby-telnet/src/config.ts new file mode 100644 index 00000000..6931a786 --- /dev/null +++ b/tabby-telnet/src/config.ts @@ -0,0 +1,12 @@ +import { ConfigProvider } from 'tabby-core' + +/** @hidden */ +export class TelnetConfigProvider extends ConfigProvider { + defaults = { + hotkeys: { + 'restart-telnet-session': [], + }, + } + + platformDefaults = { } +} diff --git a/tabby-telnet/src/hotkeys.ts b/tabby-telnet/src/hotkeys.ts new file mode 100644 index 00000000..9e2bb6be --- /dev/null +++ b/tabby-telnet/src/hotkeys.ts @@ -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 { + return this.hotkeys + } +} diff --git a/tabby-telnet/src/index.ts b/tabby-telnet/src/index.ts new file mode 100644 index 00000000..8a18a7fc --- /dev/null +++ b/tabby-telnet/src/index.ts @@ -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 diff --git a/tabby-telnet/src/profiles.ts b/tabby-telnet/src/profiles.ts new file mode 100644 index 00000000..26f88558 --- /dev/null +++ b/tabby-telnet/src/profiles.ts @@ -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 { + 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> { + 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', + }, + } + } +} diff --git a/tabby-telnet/src/recoveryProvider.ts b/tabby-telnet/src/recoveryProvider.ts new file mode 100644 index 00000000..2eec6049 --- /dev/null +++ b/tabby-telnet/src/recoveryProvider.ts @@ -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 { + async applicableTo (recoveryToken: RecoveryToken): Promise { + return recoveryToken.type === 'app:telnet-tab' + } + + async recover (recoveryToken: RecoveryToken): Promise> { + return { + type: TelnetTabComponent, + inputs: { + profile: recoveryToken['profile'], + savedState: recoveryToken['savedState'], + }, + } + } + + duplicate (recoveryToken: RecoveryToken): RecoveryToken { + return { + ...recoveryToken, + savedState: null, + } + } +} diff --git a/tabby-telnet/src/session.ts b/tabby-telnet/src/session.ts new file mode 100644 index 00000000..4e27a5a1 --- /dev/null +++ b/tabby-telnet/src/session.ts @@ -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 { return this.serviceMessage } + + private serviceMessage = new Subject() + 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 { + 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 { + this.serviceMessage.complete() + this.kill() + await super.destroy() + } + + async getChildProcesses (): Promise { + return [] + } + + async gracefullyKillProcess (): Promise { + this.kill() + } + + supportsWorkingDirectory (): boolean { + return false + } + + async getWorkingDirectory (): Promise { + return null + } +} diff --git a/tabby-telnet/tsconfig.json b/tabby-telnet/tsconfig.json new file mode 100644 index 00000000..a863b98a --- /dev/null +++ b/tabby-telnet/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["node_modules", "dist", "typings"], + "compilerOptions": { + "baseUrl": "src" + } +} diff --git a/tabby-telnet/tsconfig.typings.json b/tabby-telnet/tsconfig.typings.json new file mode 100644 index 00000000..891a0f24 --- /dev/null +++ b/tabby-telnet/tsconfig.typings.json @@ -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/*"] + } + } +} diff --git a/tabby-telnet/webpack.config.js b/tabby-telnet/webpack.config.js new file mode 100644 index 00000000..f645567c --- /dev/null +++ b/tabby-telnet/webpack.config.js @@ -0,0 +1,5 @@ +const config = require('../webpack.plugin.config') +module.exports = config({ + name: 'telnet', + dirname: __dirname +}) diff --git a/tabby-telnet/yarn.lock b/tabby-telnet/yarn.lock new file mode 100644 index 00000000..600652ec --- /dev/null +++ b/tabby-telnet/yarn.lock @@ -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== diff --git a/tabby-terminal/src/api/streamProcessing.ts b/tabby-terminal/src/api/streamProcessing.ts index 11860809..bb774761 100644 --- a/tabby-terminal/src/api/streamProcessing.ts +++ b/tabby-terminal/src/api/streamProcessing.ts @@ -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 { diff --git a/tabby-terminal/src/components/streamProcessingSettings.component.ts b/tabby-terminal/src/components/streamProcessingSettings.component.ts index e0f5cd29..be2ff872 100644 --- a/tabby-terminal/src/components/streamProcessingSettings.component.ts +++ b/tabby-terminal/src/components/streamProcessingSettings.component.ts @@ -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' }, ] diff --git a/tabby-terminal/src/session.ts b/tabby-terminal/src/session.ts index 56410461..1b8bab93 100644 --- a/tabby-terminal/src/session.ts +++ b/tabby-terminal/src/session.ts @@ -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 diff --git a/tabby-web/src/platform.ts b/tabby-web/src/platform.ts index 4bdf4c5f..29ccd938 100644 --- a/tabby-web/src/platform.ts +++ b/tabby-web/src/platform.ts @@ -91,7 +91,6 @@ export class WebPlatformService extends PlatformService { } async showMessageBox (options: MessageBoxOptions): Promise { - console.log(options) const modal = this.ngbModal.open(MessageBoxModalComponent, { backdrop: 'static', }) diff --git a/webpack.config.js b/webpack.config.js index f2f5a52b..fb1fedbd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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))