diff --git a/app/package.json b/app/package.json index 3dc464f8..e2a33950 100644 --- a/app/package.json +++ b/app/package.json @@ -31,7 +31,7 @@ "keytar": "^6.0.1", "mz": "^2.7.0", "ngx-toastr": "^12.0.1", - "node-pty": "^0.10.0-beta9", + "@terminus-term/node-pty": "0.10.0-beta9", "npm": "6.9.0", "path": "0.12.7", "rxjs": "^6.5.5", diff --git a/app/yarn.lock b/app/yarn.lock index 75a86874..0055bf33 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -108,6 +108,13 @@ dependencies: debug "^4.1.1" +"@terminus-term/node-pty@0.10.0-beta.9": + version "0.10.0-beta9" + resolved "https://registry.yarnpkg.com/@terminus-term/node-pty/-/node-pty-0.10.0-beta9.tgz#b4caff6b069139add9be959e00b364f8fe3c620d" + integrity sha512-wnttx12b9gxP9CPB9uqBMQx/Vp4EboUDGOY3xRP0Nvhec6pSF2qFZD6bwMbNzFIopbaohluEYcbEul0jTQcdeQ== + dependencies: + nan "^2.13.2" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1763,12 +1770,12 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -macos-native-processlist@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/macos-native-processlist/-/macos-native-processlist-1.0.2.tgz#78767e4fdea3eea782bbf063dac8c1e2420786dc" - integrity sha512-ShX+vFA44eaJ9/JCPTrhL0j4KLVKrYjeTQCFuR0kx7qWxETBDbFX8I3WKKXeALMtWSD+F4TDpza2mwCcE2tWAw== +macos-native-processlist@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/macos-native-processlist/-/macos-native-processlist-2.0.0.tgz#135e24612c4b0f1f16a51526bd040d5026143d38" + integrity sha512-ciW1n2jL+9f7+9xijMMVmVzRbpVlKjzMFmgpNricm5QKfkUTMEa1suR0pc9pQRrLea4sJtbLo0u+izzHDvkpIA== dependencies: - nan "^2.13.2" + node-addon-api "3.0.0" make-dir@^1.0.0: version "1.3.0" @@ -1924,7 +1931,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.13.2, nan@^2.14.0, nan@^2.14.1: +nan@^2.13.2, nan@^2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== @@ -1946,7 +1953,7 @@ node-abi@^2.18.0, node-abi@^2.7.0: dependencies: semver "^5.4.1" -node-addon-api@^3.0.0: +node-addon-api@3.0.0, node-addon-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== @@ -1995,13 +2002,6 @@ node-gyp@^4.0.0: tar "^4.4.8" which "1" -node-pty@^0.10.0-beta9: - version "0.10.0-beta9" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0-beta9.tgz#e5a795f9b53948346803cb71bac4ffc02e7909f0" - integrity sha512-Qm6uSH30FUcAhJ9s76C+lgvTsOW2cHUbkIGjCdOVCL0c7S4DxsmKBRgjcr+guUK9d9KwfuZHeSjXYWjpJFPe4w== - dependencies: - nan "^2.14.0" - noop-logger@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" diff --git a/terminus-ssh/src/components/sshSettingsTab.component.pug b/terminus-ssh/src/components/sshSettingsTab.component.pug index bb17db23..a6c68927 100644 --- a/terminus-ssh/src/components/sshSettingsTab.component.pug +++ b/terminus-ssh/src/components/sshSettingsTab.component.pug @@ -36,3 +36,14 @@ h3.mt-5 Options [(ngModel)]='config.store.ssh.warnOnClose', (ngModelChange)='config.save()', ) + +.form-line + .header + .title WinSCP path + .descriptions When WinSCP is detected, you can launch an SCP session from the context menu. + input.form-control( + type='text', + placeholder='Auto-detect', + [(ngModel)]='config.store.ssh.winSCPPath', + (ngModelChange)='config.save()', + ) diff --git a/terminus-ssh/src/config.ts b/terminus-ssh/src/config.ts index f4f7888e..399eb61c 100644 --- a/terminus-ssh/src/config.ts +++ b/terminus-ssh/src/config.ts @@ -7,6 +7,7 @@ export class SSHConfigProvider extends ConfigProvider { connections: [], recentConnections: [], warnOnClose: false, + winSCPPath: null, }, hotkeys: { ssh: [ diff --git a/terminus-ssh/src/index.ts b/terminus-ssh/src/index.ts index 6d2e0fe8..f05b6dd4 100644 --- a/terminus-ssh/src/index.ts +++ b/terminus-ssh/src/index.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { ToastrModule } from 'ngx-toastr' -import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider, HotkeyProvider } from 'terminus-core' +import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider, HotkeyProvider, TabContextMenuItemProvider } from 'terminus-core' import { SettingsTabProvider } from 'terminus-settings' import TerminusTerminalModule from 'terminus-terminal' @@ -18,6 +18,7 @@ import { SSHConfigProvider } from './config' import { SSHSettingsTabProvider } from './settings' import { RecoveryProvider } from './recoveryProvider' import { SSHHotkeyProvider } from './hotkeys' +import { WinSCPContextMenu } from './winSCPIntegration' /** @hidden */ @NgModule({ @@ -35,6 +36,7 @@ import { SSHHotkeyProvider } from './hotkeys' { provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, { provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true }, + { provide: TabContextMenuItemProvider, useClass: WinSCPContextMenu, multi: true }, ], entryComponents: [ EditConnectionModalComponent, diff --git a/terminus-ssh/src/winSCPIntegration.ts b/terminus-ssh/src/winSCPIntegration.ts new file mode 100644 index 00000000..4b8a068a --- /dev/null +++ b/terminus-ssh/src/winSCPIntegration.ts @@ -0,0 +1,85 @@ +import { execFile } from 'child_process' +import { Injectable } from '@angular/core' +import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, HostAppService, Platform } from 'terminus-core' +import { SSHTabComponent } from './components/sshTab.component' +import { PasswordStorageService } from './services/passwordStorage.service' +import { SSHConnection } from './api' + + +/* eslint-disable block-scoped-var */ +try { + var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var +} catch { } + + +/** @hidden */ +@Injectable() +export class WinSCPContextMenu extends TabContextMenuItemProvider { + weight = 10 + private detectedPath?: string + + constructor ( + private hostApp: HostAppService, + private config: ConfigService, + private passwordStorage: PasswordStorageService, + ) { + super() + + if (hostApp.platform !== Platform.Windows) { + return + } + + const key = wnr.getRegistryKey(wnr.HK.CR, 'WinSCP.Url\\DefaultIcon') + if (key?.['']) { + this.detectedPath = key[''].value?.split(',')[0] + this.detectedPath = this.detectedPath?.substring(1, this.detectedPath.length - 1) + } + } + + async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise { + if (this.hostApp.platform !== Platform.Windows || tabHeader) { + return [] + } + if (!this.getPath()) { + return [] + } + if (!(tab instanceof SSHTabComponent)) { + return [] + } + return [ + { + label: 'Launch WinSCP', + click: (): void => { + this.launchWinSCP(tab.connection) + }, + }, + ] + } + + getPath (): string|undefined { + return this.detectedPath ?? this.config.store.ssh.winSCPPath + } + + async getURI (connection: SSHConnection): Promise { + let uri = `scp://${connection.user}` + const password = await this.passwordStorage.loadPassword(connection) + if (password) { + uri += ':' + encodeURIComponent(password) + } + uri += `@${connection.host}:${connection.port}/` + return uri + } + + async launchWinSCP (connection: SSHConnection): Promise { + const path = this.getPath() + if (!path) { + return + } + let args = [await this.getURI(connection)] + if ((!connection.auth || connection.auth === 'publicKey') && connection.privateKey) { + args.push('/privatekey') + args.push(connection.privateKey) + } + execFile(path, args) + } +} diff --git a/terminus-ssh/webpack.config.js b/terminus-ssh/webpack.config.js index 40ba1dc8..b0ef1451 100644 --- a/terminus-ssh/webpack.config.js +++ b/terminus-ssh/webpack.config.js @@ -45,10 +45,12 @@ module.exports = { ], }, externals: [ + 'child_process', 'fs', 'keytar', 'path', 'ngx-toastr', + 'windows-native-registry', 'windows-process-tree/build/Release/windows_process_tree.node', /^rxjs/, /^@angular/, diff --git a/terminus-terminal/src/services/sessions.service.ts b/terminus-terminal/src/services/sessions.service.ts index 08a1873f..f62ac8b9 100644 --- a/terminus-terminal/src/services/sessions.service.ts +++ b/terminus-terminal/src/services/sessions.service.ts @@ -1,7 +1,7 @@ import * as psNode from 'ps-node' import * as fs from 'mz/fs' import * as os from 'os' -import * as nodePTY from 'node-pty' +import * as nodePTY from '@terminus-term/node-pty' import { Observable, Subject } from 'rxjs' import { first } from 'rxjs/operators' diff --git a/terminus-terminal/webpack.config.js b/terminus-terminal/webpack.config.js index 9fcb8261..10b4b37d 100644 --- a/terminus-terminal/webpack.config.js +++ b/terminus-terminal/webpack.config.js @@ -65,7 +65,7 @@ module.exports = { 'path', 'macos-native-processlist', 'windows-native-registry', - 'node-pty', + '@terminus-term/node-pty', 'windows-process-tree', 'os', /^rxjs/,