From fa07fdcb64fb34a218700bbfd81ebce1b2e26e02 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 16 May 2021 15:44:04 +0200 Subject: [PATCH] made cli handling extensible - fixes #3763 --- terminus-core/package.json | 2 - terminus-core/src/api/cli.ts | 12 ++ terminus-core/src/api/index.ts | 1 + terminus-core/src/cli.ts | 20 +++ terminus-core/src/index.ts | 3 + terminus-core/src/services/hostApp.service.ts | 69 ++------- terminus-core/yarn.lock | 10 -- terminus-terminal/package.json | 2 + terminus-terminal/src/cli.ts | 135 ++++++++++++++++++ terminus-terminal/src/index.ts | 64 +-------- terminus-terminal/yarn.lock | 10 ++ 11 files changed, 203 insertions(+), 125 deletions(-) create mode 100644 terminus-core/src/api/cli.ts create mode 100644 terminus-core/src/cli.ts create mode 100644 terminus-terminal/src/cli.ts diff --git a/terminus-core/package.json b/terminus-core/package.json index 8b85df13..4beedb41 100644 --- a/terminus-core/package.json +++ b/terminus-core/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@electron/remote": "1.1.0", "@types/js-yaml": "^4.0.0", - "@types/shell-escape": "^0.2.0", "@types/winston": "^2.3.6", "axios": "^0.21.1", "bootstrap": "^4.1.3", @@ -32,7 +31,6 @@ "ng2-dnd": "^5.0.2", "ngx-perfect-scrollbar": "^10.1.0", "readable-stream": "3.6.0", - "shell-escape": "^0.2.0", "uuid": "^8.0.0", "winston": "^3.3.3" }, diff --git a/terminus-core/src/api/cli.ts b/terminus-core/src/api/cli.ts new file mode 100644 index 00000000..e1752556 --- /dev/null +++ b/terminus-core/src/api/cli.ts @@ -0,0 +1,12 @@ +export interface CLIEvent { + argv: any + cwd: string + secondInstance: boolean +} + +export abstract class CLIHandler { + priority: number + firstMatchOnly: boolean + + abstract handle (event: CLIEvent): Promise +} diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index 53412666..b1d15520 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -9,6 +9,7 @@ export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider' export { Theme } from './theme' export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { SelectorOption } from './selector' +export { CLIHandler, CLIEvent } from './cli' export { AppService } from '../services/app.service' export { ConfigService } from '../services/config.service' diff --git a/terminus-core/src/cli.ts b/terminus-core/src/cli.ts new file mode 100644 index 00000000..418a312d --- /dev/null +++ b/terminus-core/src/cli.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core' +import { HostAppService } from './services/hostApp.service' +import { CLIHandler, CLIEvent } from './api/cli' + +@Injectable() +export class LastCLIHandler extends CLIHandler { + firstMatchOnly = true + priority = -999 + + constructor (private hostApp: HostAppService) { + super() + } + + async handle (event: CLIEvent): Promise { + if (event.secondInstance) { + this.hostApp.newWindow() + } + return true + } +} diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 04d5d933..82692c79 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -27,6 +27,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' import { HotkeyProvider } from './api/hotkeyProvider' import { ConfigProvider } from './api/configProvider' import { Theme } from './api/theme' +import { CLIHandler } from './api/cli' import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' import { TabRecoveryProvider } from './api/tabRecovery' @@ -37,6 +38,7 @@ import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { CoreConfigProvider } from './config' import { AppHotkeyProvider } from './hotkeys' import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu' +import { LastCLIHandler } from './cli' import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'ng2-dnd/bundles/style.css' @@ -51,6 +53,7 @@ const PROVIDERS = [ { provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, + { provide: CLIHandler, useClass: LastCLIHandler, multi: true }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }, ] diff --git a/terminus-core/src/services/hostApp.service.ts b/terminus-core/src/services/hostApp.service.ts index 25be96e7..fe0388af 100644 --- a/terminus-core/src/services/hostApp.service.ts +++ b/terminus-core/src/services/hostApp.service.ts @@ -1,11 +1,9 @@ import type { BrowserWindow, TouchBar, MenuItemConstructorOptions } from 'electron' -import * as path from 'path' -import * as fs from 'mz/fs' -import shellEscape from 'shell-escape' import { Observable, Subject } from 'rxjs' -import { Injectable, NgZone, EventEmitter } from '@angular/core' +import { Injectable, NgZone, EventEmitter, Injector } from '@angular/core' import { ElectronService } from './electron.service' import { Logger, LogService } from './log.service' +import { CLIHandler } from '../api/cli' import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils' /* eslint-disable block-scoped-var */ @@ -42,11 +40,6 @@ export class HostAppService { isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE private preferencesMenu = new Subject() - private secondInstance = new Subject() - private cliOpenDirectory = new Subject() - private cliRunCommand = new Subject() - private cliPaste = new Subject() - private cliOpenProfile = new Subject() private configChangeBroadcast = new Subject() private windowCloseRequest = new Subject() private windowMoved = new Subject() @@ -61,31 +54,6 @@ export class HostAppService { */ get preferencesMenu$ (): Observable { return this.preferencesMenu } - /** - * Fired when a second instance of Terminus is launched - */ - get secondInstance$ (): Observable { return this.secondInstance } - - /** - * Fired for the `terminus open` CLI command - */ - get cliOpenDirectory$ (): Observable { return this.cliOpenDirectory } - - /** - * Fired for the `terminus run` CLI command - */ - get cliRunCommand$ (): Observable { return this.cliRunCommand } - - /** - * Fired for the `terminus paste` CLI command - */ - get cliPaste$ (): Observable { return this.cliPaste } - - /** - * Fired for the `terminus profile` CLI command - */ - get cliOpenProfile$ (): Observable { return this.cliOpenProfile } - /** * Fired when another window modified the config file */ @@ -107,6 +75,7 @@ export class HostAppService { private constructor ( private zone: NgZone, private electron: ElectronService, + injector: Injector, log: LogService, ) { this.logger = log.create('hostApp') @@ -159,28 +128,18 @@ export class HostAppService { electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => { this.logger.info('CLI arguments received:', argv) - const op = argv._[0] - const opAsPath = op ? path.resolve(cwd, op) : null - if (op === 'open') { - this.cliOpenDirectory.next(path.resolve(cwd, argv.directory)) - } else if (op === 'run') { - this.cliRunCommand.next(argv.command) - } else if (op === 'paste') { - let text = argv.text - if (argv.escape) { - text = shellEscape([text]) - } - this.cliPaste.next(text) - } else if (op === 'profile') { - this.cliOpenProfile.next(argv.profileName) - } else if (secondInstance && op === undefined) { - this.newWindow() - } else if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) { - this.cliOpenDirectory.next(opAsPath) - } - if (secondInstance) { - this.secondInstance.next() + const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[] + cliHandlers.sort((a, b) => b.priority - a.priority) + + let handled = false + for (const handler of cliHandlers) { + if (handled && handler.firstMatchOnly) { + continue + } + if (await handler.handle({ argv, cwd, secondInstance })) { + handled = true + } } })) diff --git a/terminus-core/yarn.lock b/terminus-core/yarn.lock index a10b9fc3..78472649 100644 --- a/terminus-core/yarn.lock +++ b/terminus-core/yarn.lock @@ -26,11 +26,6 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.5.tgz#74deebbbcb1e86634dbf10a5b5e8798626f5a597" integrity sha512-iotVxtCCsPLRAvxMFFgxL8HD2l4mAZ2Oin7/VJ2ooWO0VOK4EGOGmZWZn1uCq7RofR3I/1IOSjCHlFT71eVK0Q== -"@types/shell-escape@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2" - integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g== - "@types/winston@^2.3.6": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/winston/-/winston-2.4.4.tgz#48cc744b7b42fad74b9a2e8490e0112bd9a3d08d" @@ -410,11 +405,6 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" -shell-escape@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133" - integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM= - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" diff --git a/terminus-terminal/package.json b/terminus-terminal/package.json index 5a9a0291..51ae3d37 100644 --- a/terminus-terminal/package.json +++ b/terminus-terminal/package.json @@ -22,12 +22,14 @@ }, "devDependencies": { "@types/deep-equal": "^1.0.0", + "@types/shell-escape": "^0.2.0", "ansi-colors": "^4.1.1", "dataurl": "0.1.0", "deep-equal": "2.0.5", "mz": "^2.6.0", "ps-node": "^0.1.6", "runes": "^0.4.2", + "shell-escape": "^0.2.0", "slugify": "^1.4.0", "utils-decorators": "^1.8.1", "xterm": "^4.9.0-beta.7", diff --git a/terminus-terminal/src/cli.ts b/terminus-terminal/src/cli.ts new file mode 100644 index 00000000..a8ac123e --- /dev/null +++ b/terminus-terminal/src/cli.ts @@ -0,0 +1,135 @@ +import * as path from 'path' +import * as fs from 'mz/fs' +import shellEscape from 'shell-escape' +import { Injectable } from '@angular/core' +import { CLIHandler, CLIEvent, HostAppService, AppService, ConfigService } from 'terminus-core' +import { TerminalTabComponent } from './components/terminalTab.component' +import { TerminalService } from './services/terminal.service' + +@Injectable() +export class TerminalCLIHandler extends CLIHandler { + firstMatchOnly = true + priority = 0 + + constructor ( + private app: AppService, + private config: ConfigService, + private hostApp: HostAppService, + private terminal: TerminalService, + ) { + super() + } + + async handle (event: CLIEvent): Promise { + const op = event.argv._[0] + + if (op === 'open') { + this.handleOpenDirectory(path.resolve(event.cwd, event.argv.directory)) + } else if (op === 'run') { + this.handleRunCommand(event.argv.command) + } else if (op === 'paste') { + let text = event.argv.text + if (event.argv.escape) { + text = shellEscape([text]) + } + this.handlePaste(text) + } else if (op === 'profile') { + this.handleOpenProfile(event.argv.profileName) + } else { + return false + } + + return true + } + + private async handleOpenDirectory (directory: string) { + if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) { + directory = directory.substring(0, directory.length - 1) + } + if (await fs.exists(directory)) { + if ((await fs.stat(directory)).isDirectory()) { + this.terminal.openTab(undefined, directory) + this.hostApp.bringToFront() + } + } + } + + private handleRunCommand (command: string[]) { + this.terminal.openTab({ + name: '', + sessionOptions: { + command: command[0], + args: command.slice(1), + }, + }, null, true) + this.hostApp.bringToFront() + } + + private handleOpenProfile (profileName: string) { + const profile = this.config.store.terminal.profiles.find(x => x.name === profileName) + if (!profile) { + console.error('Requested profile', profileName, 'not found') + return + } + this.terminal.openTabWithOptions(profile.sessionOptions) + this.hostApp.bringToFront() + } + + private handlePaste (text: string) { + if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) { + this.app.activeTab.sendInput(text) + this.hostApp.bringToFront() + } + } +} + + +@Injectable() +export class OpenPathCLIHandler extends CLIHandler { + firstMatchOnly = true + priority = -100 + + constructor ( + private terminal: TerminalService, + private hostApp: HostAppService, + ) { + super() + } + + async handle (event: CLIEvent): Promise { + const op = event.argv._[0] + const opAsPath = op ? path.resolve(event.cwd, op) : null + + if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) { + this.terminal.openTab(undefined, opAsPath) + this.hostApp.bringToFront() + return true + } + + return false + } +} + +@Injectable() +export class AutoOpenTabCLIHandler extends CLIHandler { + firstMatchOnly = true + priority = -1000 + + constructor ( + private app: AppService, + private config: ConfigService, + private terminal: TerminalService, + ) { + super() + } + + async handle (event: CLIEvent): Promise { + if (!event.secondInstance && this.config.store.terminal.autoOpen) { + this.app.ready$.subscribe(() => { + this.terminal.openTab() + }) + return true + } + return false + } +} diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index da958221..589cec5d 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -1,12 +1,10 @@ -import * as fs from 'mz/fs' - import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' import { FormsModule } from '@angular/forms' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { ToastrModule } from 'ngx-toastr' -import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService, TabContextMenuItemProvider, ElectronService } from 'terminus-core' +import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler } from 'terminus-core' import { SettingsTabProvider } from 'terminus-settings' import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component' @@ -57,6 +55,7 @@ import { hterm } from './frontends/hterm' import { Frontend } from './frontends/frontend' import { HTermFrontend } from './frontends/htermFrontend' import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend' +import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from './cli' /** @hidden */ @NgModule({ @@ -100,6 +99,10 @@ import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend' { provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: LegacyContextMenu, multi: true }, + { provide: CLIHandler, useClass: TerminalCLIHandler, multi: true }, + { provide: CLIHandler, useClass: OpenPathCLIHandler, multi: true }, + { provide: CLIHandler, useClass: AutoOpenTabCLIHandler, multi: true }, + // For WindowsDefaultShellProvider PowerShellCoreShellProvider, WSLShellProvider, @@ -133,13 +136,10 @@ import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend' }) export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class private constructor ( - app: AppService, - config: ConfigService, hotkeys: HotkeysService, terminal: TerminalService, hostApp: HostAppService, dockMenu: DockMenuService, - electron: ElectronService, ) { const events = [ { @@ -165,18 +165,6 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/ hotkeys.emitKeyEvent(nativeEvent) } }) - if (config.store.terminal.autoOpen) { - let argv = electron.process.argv - if (argv[0].includes('node')) { - argv = argv.slice(1) - } - - if (require('yargs/yargs')(argv.slice(1)).parse()._[0] !== 'open'){ - app.ready$.subscribe(() => { - terminal.openTab() - }) - } - } hotkeys.matchedHotkey.subscribe(async (hotkey) => { if (hotkey === 'new-tab') { @@ -193,46 +181,6 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/ } }) - hostApp.cliOpenDirectory$.subscribe(async directory => { - if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) { - directory = directory.substring(0, directory.length - 1) - } - if (await fs.exists(directory)) { - if ((await fs.stat(directory)).isDirectory()) { - terminal.openTab(undefined, directory) - hostApp.bringToFront() - } - } - }) - - hostApp.cliRunCommand$.subscribe(async command => { - terminal.openTab({ - name: '', - sessionOptions: { - command: command[0], - args: command.slice(1), - }, - }, null, true) - hostApp.bringToFront() - }) - - hostApp.cliPaste$.subscribe(text => { - if (app.activeTab instanceof TerminalTabComponent && app.activeTab.session) { - app.activeTab.sendInput(text) - hostApp.bringToFront() - } - }) - - hostApp.cliOpenProfile$.subscribe(async profileName => { - const profile = config.store.terminal.profiles.find(x => x.name === profileName) - if (!profile) { - console.error('Requested profile', profileName, 'not found') - return - } - terminal.openTabWithOptions(profile.sessionOptions) - hostApp.bringToFront() - }) - dockMenu.update() } } diff --git a/terminus-terminal/yarn.lock b/terminus-terminal/yarn.lock index 6f215ef9..fb366acc 100644 --- a/terminus-terminal/yarn.lock +++ b/terminus-terminal/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03" integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg== +"@types/shell-escape@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2" + integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g== + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -430,6 +435,11 @@ runes@^0.4.2: resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355" integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg== +shell-escape@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133" + integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM= + side-channel@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"