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/,