diff --git a/terminus-core/src/services/log.service.ts b/terminus-core/src/services/log.service.ts index a5931b50..fa3b4b10 100644 --- a/terminus-core/src/services/log.service.ts +++ b/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() diff --git a/terminus-terminal/src/api.ts b/terminus-terminal/src/api.ts index 584ae33a..2e76ba05 100644 --- a/terminus-terminal/src/api.ts +++ b/terminus-terminal/src/api.ts @@ -44,3 +44,15 @@ export interface ITerminalColorScheme { export abstract class TerminalColorSchemeProvider { abstract async getSchemes (): Promise } + +export interface IShell { + id: string + name: string + command: string + args?: string[] + env?: any +} + +export abstract class ShellProvider { + abstract async provide (): Promise +} diff --git a/terminus-terminal/src/buttonProvider.ts b/terminus-terminal/src/buttonProvider.ts index 679040f2..e5961e75 100644 --- a/terminus-terminal/src/buttonProvider.ts +++ b/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() + 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 } diff --git a/terminus-terminal/src/components/terminalSettingsTab.component.ts b/terminus-terminal/src/components/terminalSettingsTab.component.ts index 94a3d231..41c6df14 100644 --- a/terminus-terminal/src/components/terminalSettingsTab.component.ts +++ b/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(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(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(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) => { diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index ac205528..6982a765 100644 --- a/terminus-terminal/src/index.ts +++ b/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, diff --git a/terminus-terminal/src/services/shells.service.ts b/terminus-terminal/src/services/shells.service.ts deleted file mode 100644 index d63a7673..00000000 --- a/terminus-terminal/src/services/shells.service.ts +++ /dev/null @@ -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 { - if (this.hostApp.platform === Platform.macOS) { - return this.getDefaultMacOSShell() - } else { - return this.getDefaultLinuxShell() - } - } - - async getDefaultMacOSShell (): Promise { - let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString() - return shellEntry.split(' ')[1].trim() - } - - async getDefaultLinuxShell (): Promise { - 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] - } - } -} diff --git a/terminus-terminal/src/shellProviders.ts b/terminus-terminal/src/shellProviders.ts new file mode 100644 index 00000000..5ad2ee66 --- /dev/null +++ b/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 { + + } +} diff --git a/terminus-terminal/src/shells/cygwin32.ts b/terminus-terminal/src/shells/cygwin32.ts new file mode 100644 index 00000000..4d9fb095 --- /dev/null +++ b/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 { + if (this.hostApp.platform !== Platform.Windows) { + return [] + } + + let cygwinPath = await new Promise(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', + } + }] + } +} diff --git a/terminus-terminal/src/shells/cygwin64.ts b/terminus-terminal/src/shells/cygwin64.ts new file mode 100644 index 00000000..85db2ae9 --- /dev/null +++ b/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 { + if (this.hostApp.platform !== Platform.Windows) { + return [] + } + + let cygwinPath = await new Promise(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', + } + }] + } +} diff --git a/terminus-terminal/src/shells/gitBash.ts b/terminus-terminal/src/shells/gitBash.ts new file mode 100644 index 00000000..cfdecc38 --- /dev/null +++ b/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 { + if (this.hostApp.platform !== Platform.Windows) { + return [] + } + + let gitBashPath = await new Promise(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', + } + }] + } +} diff --git a/terminus-terminal/src/shells/linuxDefault.ts b/terminus-terminal/src/shells/linuxDefault.ts new file mode 100644 index 00000000..375c1e41 --- /dev/null +++ b/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 { + 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] + }] + } + } +} diff --git a/terminus-terminal/src/shells/macDefault.ts b/terminus-terminal/src/shells/macDefault.ts new file mode 100644 index 00000000..253a8231 --- /dev/null +++ b/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 { + 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() + }] + } +} diff --git a/terminus-terminal/src/shells/posix.ts b/terminus-terminal/src/shells/posix.ts new file mode 100644 index 00000000..c9f858a7 --- /dev/null +++ b/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 { + 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, + })) + } +} diff --git a/terminus-terminal/src/shells/windowsStock.ts b/terminus-terminal/src/shells/windowsStock.ts new file mode 100644 index 00000000..dd57339d --- /dev/null +++ b/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 { + 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' }, + ] + } +} diff --git a/terminus-terminal/src/shells/wsl.ts b/terminus-terminal/src/shells/wsl.ts new file mode 100644 index 00000000..65980f53 --- /dev/null +++ b/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 { + 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 + }] + } +}