From 9502240480ecbeb3df982cff30a4f71142408ed0 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Tue, 13 Jul 2021 20:19:28 +0200 Subject: [PATCH] reworked login scripts - fixes #1349 --- package.json | 3 + tabby-core/src/index.ts | 3 + .../profilesSettingsTab.component.pug | 6 +- .../sshProfileSettings.component.pug | 2 +- .../sshProfileSettings.component.ts | 5 +- tabby-terminal/package.json | 8 +- .../src/api/loginScriptProcessing.ts | 47 ++++++++---- .../loginScriptsSettings.component.pug | 74 ++++++++++--------- .../loginScriptsSettings.component.ts | 27 ++----- tabby-terminal/src/index.ts | 1 + yarn.lock | 17 +++++ 11 files changed, 117 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 82afdaa5..5025892b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/fs-extra": "^9.0.12", "@types/js-yaml": "^4.0.2", "@types/node": "16.0.1", + "@types/sortablejs": "^1.10.7", "@types/webpack-env": "^1.16.2", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", @@ -40,6 +41,7 @@ "json-loader": "0.5.7", "lru-cache": "^6.0.0", "macos-release": "^2.5.0", + "ngx-sortablejs": "^11.1.0", "ngx-toastr": "^14.0.0", "node-abi": "^2.30.0", "node-sass": "^6.0.1", @@ -55,6 +57,7 @@ "sass-loader": "^12.1.0", "shelljs": "0.8.4", "slugify": "^1.5.3", + "sortablejs": "^1.14.0", "source-code-pro": "^2.38.0", "source-map-loader": "^3.0.0", "source-sans-pro": "3.6.0", diff --git a/tabby-core/src/index.ts b/tabby-core/src/index.ts index b07823f5..d3e38b7c 100644 --- a/tabby-core/src/index.ts +++ b/tabby-core/src/index.ts @@ -6,6 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { NgxFilesizeModule } from 'ngx-filesize' import { DndModule } from 'ng2-dnd' +import { SortablejsModule } from 'ngx-sortablejs' import { AppRootComponent } from './components/appRoot.component' import { CheckboxComponent } from './components/checkbox.component' @@ -77,6 +78,7 @@ const PROVIDERS = [ NgxFilesizeModule, PerfectScrollbarModule, DndModule.forRoot(), + SortablejsModule.forRoot({ animation: 150 }), ], declarations: [ AppRootComponent as any, @@ -118,6 +120,7 @@ const PROVIDERS = [ DropZoneDirective, FastHtmlBindDirective, AlwaysVisibleTypeaheadDirective, + SortablejsModule, ], }) export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class diff --git a/tabby-settings/src/components/profilesSettingsTab.component.pug b/tabby-settings/src/components/profilesSettingsTab.component.pug index f5ff92ae..fd3d62d1 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.pug +++ b/tabby-settings/src/components/profilesSettingsTab.component.pug @@ -56,7 +56,7 @@ h3.mb-3 Profiles *ngIf='group.editable && group.name', (click)='$event.stopPropagation(); deleteGroup(group)' ) - i.fas.fa-trash + i.fas.fa-trash-alt ng-container(*ngIf='!group.collapsed') ng-container(*ngFor='let profile of group.profiles') .list-group-item.pl-5.d-flex.align-items-center( @@ -85,10 +85,10 @@ h3.mb-3 Profiles button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)') i.fas.fa-copy - button.btn.btn-link.text-danger.hover-reveal.ml-1( + button.btn.btn-link.hover-reveal.ml-1( *ngIf='!profile.isBuiltin', (click)='$event.stopPropagation(); deleteProfile(profile)' ) - i.fas.fa-trash + i.fas.fa-trash-alt .ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}} diff --git a/tabby-ssh/src/components/sshProfileSettings.component.pug b/tabby-ssh/src/components/sshProfileSettings.component.pug index 1f55062f..87cd4ada 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.pug +++ b/tabby-ssh/src/components/sshProfileSettings.component.pug @@ -189,6 +189,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav') li(ngbNavItem) a(ngbNavLink) Login scripts ng-template(ngbNavContent) - login-scripts-settings([options]='profile.options') + login-scripts-settings([options]='profile.options', #loginScriptsSettings) div([ngbNavOutlet]='nav') diff --git a/tabby-ssh/src/components/sshProfileSettings.component.ts b/tabby-ssh/src/components/sshProfileSettings.component.ts index 868130cb..a7ce5ad7 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.ts +++ b/tabby-ssh/src/components/sshProfileSettings.component.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Component } from '@angular/core' +import { Component, ViewChild } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core' +import { LoginScriptsSettingsComponent } from 'tabby-terminal' import { PasswordStorageService } from '../services/passwordStorage.service' import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api' import { SSHProfilesService } from '../profiles' @@ -20,6 +21,7 @@ export class SSHProfileSettingsComponent { supportedAlgorithms: Record = {} algorithms: Record> = {} jumpHosts: SSHProfile[] + @ViewChild('loginScriptsSettings') loginScriptsSettings: LoginScriptsSettingsComponent|null constructor ( public hostApp: HostAppService, @@ -92,6 +94,7 @@ export class SSHProfileSettingsComponent { if (!this.useProxyCommand) { this.profile.options.proxyCommand = undefined } + this.loginScriptsSettings?.save() } onForwardAdded (fw: ForwardedPortConfig) { diff --git a/tabby-terminal/package.json b/tabby-terminal/package.json index ac3a0dd4..27866e5b 100644 --- a/tabby-terminal/package.json +++ b/tabby-terminal/package.json @@ -24,17 +24,17 @@ "@types/deep-equal": "^1.0.0", "@types/shell-escape": "^0.2.0", "ansi-colors": "^4.1.1", + "binstring": "^0.2.1", + "buffer-replace": "^1.0.0", + "cli-spinner": "^0.2.10", "dataurl": "0.1.0", "deep-equal": "2.0.5", + "hexer": "^1.5.0", "ps-node": "^0.1.6", - "cli-spinner": "^0.2.10", "runes": "^0.4.2", "shell-escape": "^0.2.0", "utils-decorators": "^1.8.1", "xterm": "^4.9.0-beta.7", - "binstring": "^0.2.1", - "buffer-replace": "^1.0.0", - "hexer": "^1.5.0", "xterm-addon-fit": "^0.5.0", "xterm-addon-ligatures": "^0.5.0", "xterm-addon-search": "^0.8.0", diff --git a/tabby-terminal/src/api/loginScriptProcessing.ts b/tabby-terminal/src/api/loginScriptProcessing.ts index fd4488c7..efd88f07 100644 --- a/tabby-terminal/src/api/loginScriptProcessing.ts +++ b/tabby-terminal/src/api/loginScriptProcessing.ts @@ -1,3 +1,4 @@ +import deepClone from 'clone-deep' import { Subject, Observable } from 'rxjs' import { Logger } from 'tabby-core' @@ -18,11 +19,28 @@ export class LoginScriptProcessor { private outputToSession = new Subject() private remainingScripts: LoginScript[] = [] + private escapeSeqMap = { + a: '\x07', + b: '\x08', + e: '\x1b', + f: '\x0c', + n: '\x0a', + r: '\x0d', + t: '\x09', + v: '\x0b', + } + constructor ( private logger: Logger, options: LoginScriptsOptions ) { - this.remainingScripts = options.scripts ?? [] + this.remainingScripts = deepClone(options.scripts ?? []) + for (const script of this.remainingScripts) { + if (!script.isRegex) { + script.expect = this.unescape(script.expect) + } + script.send = this.unescape(script.send) + } } feedFromSession (data: Buffer): boolean { @@ -34,25 +52,17 @@ export class LoginScriptProcessor { continue } let match = false - let cmd = '' if (script.isRegex) { const re = new RegExp(script.expect, 'g') - if (re.exec(dataString)) { - cmd = dataString.replace(re, script.send) - match = true - found = true - } + match = re.test(dataString) } else { - if (dataString.includes(script.expect)) { - cmd = script.send - match = true - found = true - } + match = dataString.includes(script.expect) } if (match) { - this.logger.info('Executing script: "' + cmd + '"') - this.outputToSession.next(Buffer.from(cmd + '\n')) + found = true + this.logger.info('Executing script:', script) + this.outputToSession.next(Buffer.from(script.send + '\n')) this.remainingScripts = this.remainingScripts.filter(x => x !== script) } else { if (script.optional) { @@ -83,4 +93,13 @@ export class LoginScriptProcessor { } } } + + unescape (line: string): string { + line = line.replace(/\\((x\d{2})|(u\d{4}))/g, (match, g) => { + return String.fromCharCode(parseInt(g.substr(1), 16)) + }) + return line.replace(/\\(.)/g, (match, g) => { + return this.escapeSeqMap[g] || g + }) + } } diff --git a/tabby-terminal/src/components/loginScriptsSettings.component.pug b/tabby-terminal/src/components/loginScriptsSettings.component.pug index f6eba851..ad401f60 100644 --- a/tabby-terminal/src/components/loginScriptsSettings.component.pug +++ b/tabby-terminal/src/components/loginScriptsSettings.component.pug @@ -1,37 +1,43 @@ -table(*ngIf='options.scripts.length > 0') - tr - th String to expect - th String to be sent - th.pl-2 Regex - th.pl-2 Optional - th.pl-2 Actions - tr(*ngFor='let script of options.scripts') - td.pr-2 - input.form-control( - type='text', - [(ngModel)]='script.expect' - ) - td - input.form-control( - type='text', - [(ngModel)]='script.send' - ) - td.pl-2 - checkbox( - [(ngModel)]='script.isRegex', - ) - td.pl-2 - checkbox( - [(ngModel)]='script.optional', - ) - td.pl-2 - .input-group.flex-nowrap - button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') - i.fas.fa-arrow-up - button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') - i.fas.fa-arrow-down - button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') - i.fas.fa-trash +div([sortablejs]='scripts') + .input-group.flex-grow-1(*ngFor='let script of scripts') + input.form-control( + type='text', + placeholder='Expect', + [(ngModel)]='script.expect' + ) + input.form-control( + type='text', + placeholder='Send', + [(ngModel)]='script.send' + ) + .input-group-append.input-group-text.p-0.border-0 + .hover-reveal(ngbDropdown) + button.btn.btn-link.btn-sm(ngbDropdownToggle) + i.fas.fa-fw.fa-cog + .dropdown-menu-right(ngbDropdownMenu) + a.dropdown-item( + href='#', + (click)='script.isRegex = false', + [class.active]='!script.isRegex', + ) + i.fas.fa-fw([class.fa-check]='!script.isRegex') + span Exact match + a.dropdown-item( + href='#', + (click)='script.isRegex = true', + [class.active]='script.isRegex', + ) + i.fas.fa-fw([class.fa-check]='script.isRegex') + span Regex + a.dropdown-item( + href='#', + (click)='script.optional = !script.optional', + [class.active]='script.optional', + ) + i.fas.fa-fw([class.fa-check]='script.optional') + span Optional + button.btn.btn-link.btn-sm.hover-reveal((click)='deleteScript(script)') + i.fas.fa-fw.fa-trash-alt button.btn.btn-outline-info.mt-2((click)='addScript()') i.fas.fa-plus diff --git a/tabby-terminal/src/components/loginScriptsSettings.component.ts b/tabby-terminal/src/components/loginScriptsSettings.component.ts index 33f54b11..158b3cc7 100644 --- a/tabby-terminal/src/components/loginScriptsSettings.component.ts +++ b/tabby-terminal/src/components/loginScriptsSettings.component.ts @@ -11,29 +11,14 @@ import { LoginScript, LoginScriptsOptions } from '../api/loginScriptProcessing' }) export class LoginScriptsSettingsComponent { @Input() options: LoginScriptsOptions + scripts: LoginScript[] constructor ( private platform: PlatformService, ) { } ngOnInit () { - this.options.scripts ??= [] - } - - moveScriptUp (script: LoginScript) { - const index = this.options.scripts!.indexOf(script) - if (index > 0) { - this.options.scripts!.splice(index, 1) - this.options.scripts!.splice(index - 1, 0, script) - } - } - - moveScriptDown (script: LoginScript) { - const index = this.options.scripts!.indexOf(script) - if (index >= 0 && index < this.options.scripts!.length - 1) { - this.options.scripts!.splice(index, 1) - this.options.scripts!.splice(index + 1, 0, script) - } + this.scripts = this.options.scripts ?? [] } async deleteScript (script: LoginScript) { @@ -46,11 +31,15 @@ export class LoginScriptsSettingsComponent { defaultId: 1, } )).response === 1) { - this.options.scripts = this.options.scripts!.filter(x => x !== script) + this.scripts = this.scripts.filter(x => x !== script) } } addScript () { - this.options.scripts!.push({ expect: '', send: '' }) + this.scripts.push({ expect: '', send: '' }) + } + + save () { + this.options.scripts = this.scripts } } diff --git a/tabby-terminal/src/index.ts b/tabby-terminal/src/index.ts index 98b511d5..902995a3 100644 --- a/tabby-terminal/src/index.ts +++ b/tabby-terminal/src/index.ts @@ -120,3 +120,4 @@ export * from './api/interfaces' export * from './api/streamProcessing' export * from './api/loginScriptProcessing' export * from './session' +export { LoginScriptsSettingsComponent, StreamProcessingSettingsComponent } diff --git a/yarn.lock b/yarn.lock index 7d11a285..746a6917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -509,6 +509,11 @@ dependencies: "@types/node" "*" +"@types/sortablejs@^1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.7.tgz#ab9039c85429f0516955ec6dbc0bb20139417b15" + integrity sha512-lGCwwgpj8zW/ZmaueoPVSP7nnc9t8VqVWXS+ASX3eoUUENmiazv0rlXyTRludXzuX9ALjPsMqBu85TgJNWbTOg== + "@types/verror@^1.10.3": version "1.10.4" resolved "https://registry.npmjs.org/@types/verror/-/verror-1.10.4.tgz" @@ -5036,6 +5041,13 @@ neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +ngx-sortablejs@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/ngx-sortablejs/-/ngx-sortablejs-11.1.0.tgz#14e50c48db908c1cb4b37722b28c2d3867c6140a" + integrity sha512-eM4dHwWSmXDcvF5gUmyMMQ0qqcqBXWCSZ9IRpqx4UkBKfo4N7pk/QuYh5io2fXVHWVFDaxW1yhn2FNpqxV6Jqw== + dependencies: + tslib "^2.0.0" + ngx-toastr@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286" @@ -7142,6 +7154,11 @@ socks@^1.1.10: ip "^1.1.4" smart-buffer "^1.0.13" +sortablejs@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8" + integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w== + sorted-object@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz"