Browse Source

make shell providers pluggable

Eugene Pankov 8 years ago
parent
commit
1f825b16c1

+ 6 - 5
terminus-core/src/services/log.service.ts

@@ -5,14 +5,15 @@ export class Logger {
         private name: string,
     ) {}
 
-    log (level: string, ...args: any[]) {
+    doLog (level: string, ...args: any[]) {
         console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
     }
 
-    debug (...args: any[]) { this.log('debug', ...args) }
-    info (...args: any[]) { this.log('info', ...args) }
-    warn (...args: any[]) { this.log('warn', ...args) }
-    error (...args: any[]) { this.log('error', ...args) }
+    debug (...args: any[]) { this.doLog('debug', ...args) }
+    info (...args: any[]) { this.doLog('info', ...args) }
+    warn (...args: any[]) { this.doLog('warn', ...args) }
+    error (...args: any[]) { this.doLog('error', ...args) }
+    log (...args: any[]) { this.doLog('log', ...args) }
 }
 
 @Injectable()

+ 12 - 0
terminus-terminal/src/api.ts

@@ -44,3 +44,15 @@ export interface ITerminalColorScheme {
 export abstract class TerminalColorSchemeProvider {
     abstract async getSchemes (): Promise<ITerminalColorScheme[]>
 }
+
+export interface IShell {
+    id: string
+    name: string
+    command: string
+    args?: string[]
+    env?: any
+}
+
+export abstract class ShellProvider {
+    abstract async provide (): Promise<IShell[]>
+}

+ 25 - 19
terminus-terminal/src/buttonProvider.ts

@@ -1,24 +1,34 @@
+import { AsyncSubject } from 'rxjs'
 import * as fs from 'mz/fs'
 import * as path from 'path'
-import { Injectable } from '@angular/core'
-import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService, ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
+import { Injectable, Inject } from '@angular/core'
+import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService, ConfigService, HostAppService, ElectronService, Logger, LogService } from 'terminus-core'
 
+import { IShell, ShellProvider } from './api'
 import { SessionsService } from './services/sessions.service'
-import { ShellsService } from './services/shells.service'
 import { TerminalTabComponent } from './components/terminalTab.component'
 
 @Injectable()
 export class ButtonProvider extends ToolbarButtonProvider {
+    private shells$ = new AsyncSubject<IShell[]>()
+    private logger: Logger
+
     constructor (
         private app: AppService,
         private sessions: SessionsService,
         private config: ConfigService,
-        private shells: ShellsService,
-        private hostApp: HostAppService,
+        log: LogService,
+        hostApp: HostAppService,
+        @Inject(ShellProvider) shellProviders: ShellProvider[],
         electron: ElectronService,
         hotkeys: HotkeysService,
     ) {
         super()
+        this.logger = log.create('newTerminalButton')
+        Promise.all(shellProviders.map(x => x.provide())).then(shellLists => {
+            this.shells$.next(shellLists.reduce((a, b) => a.concat(b)))
+            this.shells$.complete()
+        })
         hotkeys.matchedHotkey.subscribe(async (hotkey) => {
             if (hotkey === 'new-tab') {
                 this.openNewTab()
@@ -50,24 +60,20 @@ export class ButtonProvider extends ToolbarButtonProvider {
         if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
             cwd = await this.app.activeTab.session.getWorkingDirectory()
         }
-        let command = this.config.store.terminal.shell
-        let env: any = process.env
-        let args: string[] = []
-        if (command === '~clink~') {
-            ({ command, args } = this.shells.getClinkOptions())
-        }
-        if (command === '~default-shell~') {
-            command = await this.shells.getDefaultShell()
-        }
-        if (this.hostApp.platform === Platform.Windows) {
-            env.TERM = 'cygwin'
-        }
+        let shells = await this.shells$.first().toPromise()
+        let shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
+        let env: any = Object.assign({}, process.env, shell.env || {})
+
+        this.logger.log(`Starting shell ${shell.name}`, shell)
         let sessionOptions = await this.sessions.prepareNewSession({
-            command,
-            args,
+            command: shell.command,
+            args: shell.args || [],
             cwd,
             env,
         })
+
+        this.logger.log('Using session options:', sessionOptions)
+
         this.app.openNewTab(
             TerminalTabComponent,
             { sessionOptions }

+ 3 - 77
terminus-terminal/src/components/terminalSettingsTab.component.ts

@@ -1,23 +1,11 @@
 import { Observable } from 'rxjs'
-import * as fs from 'mz/fs'
-import * as path from 'path'
 import { exec } from 'mz/child_process'
 const equal = require('deep-equal')
 const fontManager = require('font-manager')
 
 import { Component, Inject } from '@angular/core'
 import { ConfigService, HostAppService, Platform } from 'terminus-core'
-import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
-
-let Registry = null
-try {
-    Registry = require('winreg')
-} catch (_) { } // tslint:disable-line no-empty
-
-interface IShell {
-    name: string
-    command: string
-}
+import { TerminalColorSchemeProvider, ITerminalColorScheme, IShell, ShellProvider } from '../api'
 
 @Component({
     template: require('./terminalSettingsTab.component.pug'),
@@ -34,6 +22,7 @@ export class TerminalSettingsTabComponent {
     constructor (
         public config: ConfigService,
         private hostApp: HostAppService,
+        @Inject(ShellProvider) private shellProviders: ShellProvider[],
         @Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
     ) { }
 
@@ -53,71 +42,8 @@ export class TerminalSettingsTabComponent {
                 this.fonts.sort()
             })
         }
-        if (this.hostApp.platform === Platform.Windows) {
-            this.shells = [
-                { name: 'CMD (clink)', command: '~clink~' },
-                { name: 'CMD (stock)', command: 'cmd.exe' },
-                { name: 'PowerShell', command: 'powershell.exe' },
-            ]
-
-            // Detect whether BoW is installed
-            const wslPath = `${process.env.windir}\\system32\\bash.exe`
-            if (await fs.exists(wslPath)) {
-                this.shells.push({ name: 'Bash on Windows', command: wslPath })
-            }
-
-            // Detect Cygwin
-            let cygwinPath = await new Promise<string>(resolve => {
-                let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
-                reg.get('rootdir', (err, item) => {
-                    if (err) {
-                        return resolve(null)
-                    }
-                    resolve(item.value)
-                })
-            })
-            if (cygwinPath) {
-                this.shells.push({ name: 'Cygwin', command: path.join(cygwinPath, 'bin', 'bash.exe') })
-            }
-
-            // Detect 32-bit Cygwin
-            let cygwin32Path = await new Promise<string>(resolve => {
-                let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
-                reg.get('rootdir', (err, item) => {
-                    if (err) {
-                        return resolve(null)
-                    }
-                    resolve(item.value)
-                })
-            })
-            if (cygwin32Path) {
-                this.shells.push({ name: 'Cygwin (32 bit)', command: path.join(cygwin32Path, 'bin', 'bash.exe') })
-            }
-
-            // Detect Git-Bash
-            let gitBashPath = await new Promise<string>(resolve => {
-                let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
-                reg.get('InstallPath', (err, item) => {
-                    if (err) {
-                        resolve(null)
-                        return
-                    }
-                    resolve(item.value)
-                })
-            })
-            if (gitBashPath) {
-                this.shells.push({ name: 'Git-Bash', command: path.join(gitBashPath, 'bin', 'bash.exe') })
-            }
-        }
-        if (this.hostApp.platform === Platform.Linux || this.hostApp.platform === Platform.macOS) {
-            this.shells = [{ name: 'Default shell', command: '~default-shell~' }]
-            this.shells = this.shells.concat((await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
-                .split('\n')
-                .map(x => x.trim())
-                .filter(x => x && !x.startsWith('#'))
-                .map(x => ({ name: x, command: x })))
-        }
         this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
+        this.shells = (await Promise.all(this.shellProviders.map(x => x.provide()))).reduce((a, b) => a.concat(b))
     }
 
     fontAutocomplete = (text$: Observable<string>) => {

+ 20 - 3
terminus-terminal/src/index.ts

@@ -11,18 +11,27 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
 import { ColorPickerComponent } from './components/colorPicker.component'
 
 import { SessionsService } from './services/sessions.service'
-import { ShellsService } from './services/shells.service'
 
 import { ScreenPersistenceProvider } from './persistenceProviders'
 import { TMuxPersistenceProvider } from './tmux'
 import { ButtonProvider } from './buttonProvider'
 import { RecoveryProvider } from './recoveryProvider'
-import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator } from './api'
+import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
 import { TerminalSettingsTabProvider } from './settings'
 import { PathDropDecorator } from './pathDrop'
 import { TerminalConfigProvider } from './config'
 import { TerminalHotkeyProvider } from './hotkeys'
 import { HyperColorSchemes } from './colorSchemes'
+
+import { Cygwin32ShellProvider } from './shells/cygwin32'
+import { Cygwin64ShellProvider } from './shells/cygwin64'
+import { GitBashShellProvider } from './shells/gitBash'
+import { LinuxDefaultShellProvider } from './shells/linuxDefault'
+import { MacOSDefaultShellProvider } from './shells/macDefault'
+import { POSIXShellsProvider } from './shells/posix'
+import { WindowsStockShellsProvider } from './shells/windowsStock'
+import { WSLShellProvider } from './shells/wsl'
+
 import { hterm } from './hterm'
 
 @NgModule({
@@ -33,7 +42,6 @@ import { hterm } from './hterm'
     ],
     providers: [
         SessionsService,
-        ShellsService,
         ScreenPersistenceProvider,
         TMuxPersistenceProvider,
         { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
@@ -63,6 +71,15 @@ import { hterm } from './hterm'
         { provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
         { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
         { provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
+
+        { provide: ShellProvider, useClass: WindowsStockShellsProvider, multi: true },
+        { provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
+        { provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
+        { provide: ShellProvider, useClass: Cygwin32ShellProvider, multi: true },
+        { provide: ShellProvider, useClass: Cygwin64ShellProvider, multi: true },
+        { provide: ShellProvider, useClass: GitBashShellProvider, multi: true },
+        { provide: ShellProvider, useClass: POSIXShellsProvider, multi: true },
+        { provide: ShellProvider, useClass: WSLShellProvider, multi: true },
     ],
     entryComponents: [
         TerminalTabComponent,

+ 0 - 58
terminus-terminal/src/services/shells.service.ts

@@ -1,58 +0,0 @@
-import * as path from 'path'
-import { exec } from 'mz/child_process'
-import * as fs from 'mz/fs'
-import { Injectable } from '@angular/core'
-import { ElectronService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
-
-@Injectable()
-export class ShellsService {
-    private logger: Logger
-
-    constructor (
-        log: LogService,
-        private electron: ElectronService,
-        private hostApp: HostAppService,
-    ) {
-        this.logger = log.create('shells')
-    }
-
-    getClinkOptions (): { command, args } {
-        return {
-            command: 'cmd.exe',
-            args: [
-                '/k',
-                path.join(
-                    path.dirname(this.electron.app.getPath('exe')),
-                    'resources',
-                    'clink',
-                    `clink_${process.arch}.exe`,
-                ),
-                'inject',
-            ]
-        }
-    }
-
-    async getDefaultShell (): Promise<string> {
-        if (this.hostApp.platform === Platform.macOS) {
-            return this.getDefaultMacOSShell()
-        } else {
-            return this.getDefaultLinuxShell()
-        }
-    }
-
-    async getDefaultMacOSShell (): Promise<string> {
-        let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
-        return shellEntry.split(' ')[1].trim()
-    }
-
-    async getDefaultLinuxShell (): Promise<string> {
-        let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
-            .split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
-        if (!line) {
-            this.logger.warn('Could not detect user shell')
-            return '/bin/sh'
-        } else {
-            return line.split(':')[6]
-        }
-    }
-}

+ 19 - 0
terminus-terminal/src/shellProviders.ts

@@ -0,0 +1,19 @@
+import * as fs from 'mz/fs'
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
+
+import { ShellProvider, IShell } from './api'
+
+@Injectable()
+export class POSIXShellsProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+
+    }
+}

+ 48 - 0
terminus-terminal/src/shells/cygwin32.ts

@@ -0,0 +1,48 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+    Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class Cygwin32ShellProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Windows) {
+            return []
+        }
+
+        let cygwinPath = await new Promise<string>(resolve => {
+            let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
+            reg.get('rootdir', (err, item) => {
+                if (err) {
+                    return resolve(null)
+                }
+                resolve(item.value)
+            })
+        })
+
+        if (!cygwinPath) {
+            return []
+        }
+
+        return [{
+            id: 'cygwin32',
+            name: 'Cygwin (32 bit)',
+            command: path.join(cygwinPath, 'bin', 'bash.exe'),
+            env: {
+                TERM: 'cygwin',
+            }
+        }]
+    }
+}

+ 48 - 0
terminus-terminal/src/shells/cygwin64.ts

@@ -0,0 +1,48 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+    Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class Cygwin64ShellProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Windows) {
+            return []
+        }
+
+        let cygwinPath = await new Promise<string>(resolve => {
+            let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
+            reg.get('rootdir', (err, item) => {
+                if (err) {
+                    return resolve(null)
+                }
+                resolve(item.value)
+            })
+        })
+
+        if (!cygwinPath) {
+            return []
+        }
+
+        return [{
+            id: 'cygwin64',
+            name: 'Cygwin',
+            command: path.join(cygwinPath, 'bin', 'bash.exe'),
+            env: {
+                TERM: 'cygwin',
+            }
+        }]
+    }
+}

+ 49 - 0
terminus-terminal/src/shells/gitBash.ts

@@ -0,0 +1,49 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+    Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class GitBashShellProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Windows) {
+            return []
+        }
+
+        let gitBashPath = await new Promise<string>(resolve => {
+            let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
+            reg.get('InstallPath', (err, item) => {
+                if (err) {
+                    resolve(null)
+                    return
+                }
+                resolve(item.value)
+            })
+        })
+
+        if (!gitBashPath) {
+            return []
+        }
+
+        return [{
+            id: 'git-bash',
+            name: 'Git-Bash',
+            command: path.join(gitBashPath, 'bin', 'bash.exe'),
+            env: {
+                TERM: 'cygwin',
+            }
+        }]
+    }
+}

+ 40 - 0
terminus-terminal/src/shells/linuxDefault.ts

@@ -0,0 +1,40 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform, LogService, Logger } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class LinuxDefaultShellProvider extends ShellProvider {
+    private logger: Logger
+
+    constructor (
+        private hostApp: HostAppService,
+        log: LogService,
+    ) {
+        super()
+        this.logger = log.create('linuxDefaultShell')
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Linux) {
+            return []
+        }
+        let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
+            .split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
+        if (!line) {
+            this.logger.warn('Could not detect user shell')
+            return [{
+                id: 'default',
+                name: 'User default',
+                command: '/bin/sh'
+            }]
+        } else {
+            return [{
+                id: 'default',
+                name: 'User default',
+                command: line.split(':')[6]
+            }]
+        }
+    }
+}

+ 26 - 0
terminus-terminal/src/shells/macDefault.ts

@@ -0,0 +1,26 @@
+import { exec } from 'mz/child_process'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class MacOSDefaultShellProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.macOS) {
+            return []
+        }
+        let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
+        return [{
+            id: 'default',
+            name: 'User default',
+            command: shellEntry.split(' ')[1].trim()
+        }]
+    }
+}

+ 29 - 0
terminus-terminal/src/shells/posix.ts

@@ -0,0 +1,29 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class POSIXShellsProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform === Platform.Windows) {
+            return []
+        }
+        return (await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
+            .split('\n')
+            .map(x => x.trim())
+            .filter(x => x && !x.startsWith('#'))
+            .map(x => ({
+                id: x,
+                name: x,
+                command: x,
+            }))
+    }
+}

+ 40 - 0
terminus-terminal/src/shells/windowsStock.ts

@@ -0,0 +1,40 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform, ElectronService } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class WindowsStockShellsProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+        private electron: ElectronService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Windows) {
+            return []
+        }
+        return [
+            {
+                id: 'clink',
+                name: 'CMD (clink)',
+                command: 'cmd.exe',
+                args: [
+                    '/k',
+                    path.join(
+                        path.dirname(this.electron.app.getPath('exe')),
+                        'resources',
+                        'clink',
+                        `clink_${process.arch}.exe`,
+                    ),
+                    'inject',
+                ]
+            },
+            { id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
+            { id: 'powershell', name: 'PowerShell', command: 'powershell.exe' },
+        ]
+    }
+}

+ 31 - 0
terminus-terminal/src/shells/wsl.ts

@@ -0,0 +1,31 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class WSLShellProvider extends ShellProvider {
+    constructor (
+        private hostApp: HostAppService,
+    ) {
+        super()
+    }
+
+    async provide (): Promise<IShell[]> {
+        if (this.hostApp.platform !== Platform.Windows) {
+            return []
+        }
+
+        const wslPath = `${process.env.windir}\\system32\\bash.exe`
+        if (!await fs.exists(wslPath)) {
+            return []
+        }
+
+        return [{
+            id: 'wsl',
+            name: 'Bash on Windows',
+            command: wslPath
+        }]
+    }
+}