1
1
mirror of https://github.com/Eugeny/tabby.git synced 2024-11-24 06:04:04 +03:00

make shell providers pluggable

This commit is contained in:
Eugene Pankov 2017-07-30 20:58:31 +02:00
parent 17ad43bf65
commit 1f825b16c1
15 changed files with 396 additions and 162 deletions

View File

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

View File

@ -44,3 +44,15 @@ export interface ITerminalColorScheme {
export abstract class TerminalColorSchemeProvider { export abstract class TerminalColorSchemeProvider {
abstract async getSchemes (): Promise<ITerminalColorScheme[]> 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[]>
}

View File

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

View File

@ -1,23 +1,11 @@
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import * as fs from 'mz/fs'
import * as path from 'path'
import { exec } from 'mz/child_process' import { exec } from 'mz/child_process'
const equal = require('deep-equal') const equal = require('deep-equal')
const fontManager = require('font-manager') const fontManager = require('font-manager')
import { Component, Inject } from '@angular/core' import { Component, Inject } from '@angular/core'
import { ConfigService, HostAppService, Platform } from 'terminus-core' import { ConfigService, HostAppService, Platform } from 'terminus-core'
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api' import { TerminalColorSchemeProvider, ITerminalColorScheme, IShell, ShellProvider } from '../api'
let Registry = null
try {
Registry = require('winreg')
} catch (_) { } // tslint:disable-line no-empty
interface IShell {
name: string
command: string
}
@Component({ @Component({
template: require('./terminalSettingsTab.component.pug'), template: require('./terminalSettingsTab.component.pug'),
@ -34,6 +22,7 @@ export class TerminalSettingsTabComponent {
constructor ( constructor (
public config: ConfigService, public config: ConfigService,
private hostApp: HostAppService, private hostApp: HostAppService,
@Inject(ShellProvider) private shellProviders: ShellProvider[],
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[], @Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
) { } ) { }
@ -53,71 +42,8 @@ export class TerminalSettingsTabComponent {
this.fonts.sort() 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.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>) => { fontAutocomplete = (text$: Observable<string>) => {

View File

@ -11,18 +11,27 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
import { ColorPickerComponent } from './components/colorPicker.component' import { ColorPickerComponent } from './components/colorPicker.component'
import { SessionsService } from './services/sessions.service' import { SessionsService } from './services/sessions.service'
import { ShellsService } from './services/shells.service'
import { ScreenPersistenceProvider } from './persistenceProviders' import { ScreenPersistenceProvider } from './persistenceProviders'
import { TMuxPersistenceProvider } from './tmux' import { TMuxPersistenceProvider } from './tmux'
import { ButtonProvider } from './buttonProvider' import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider' import { RecoveryProvider } from './recoveryProvider'
import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator } from './api' import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
import { TerminalSettingsTabProvider } from './settings' import { TerminalSettingsTabProvider } from './settings'
import { PathDropDecorator } from './pathDrop' import { PathDropDecorator } from './pathDrop'
import { TerminalConfigProvider } from './config' import { TerminalConfigProvider } from './config'
import { TerminalHotkeyProvider } from './hotkeys' import { TerminalHotkeyProvider } from './hotkeys'
import { HyperColorSchemes } from './colorSchemes' 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' import { hterm } from './hterm'
@NgModule({ @NgModule({
@ -33,7 +42,6 @@ import { hterm } from './hterm'
], ],
providers: [ providers: [
SessionsService, SessionsService,
ShellsService,
ScreenPersistenceProvider, ScreenPersistenceProvider,
TMuxPersistenceProvider, TMuxPersistenceProvider,
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
@ -63,6 +71,15 @@ import { hterm } from './hterm'
{ provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true }, { provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }, { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: TerminalDecorator, useClass: PathDropDecorator, 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: [ entryComponents: [
TerminalTabComponent, TerminalTabComponent,

View File

@ -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]
}
}
}

View File

@ -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[]> {
}
}

View File

@ -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',
}
}]
}
}

View File

@ -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',
}
}]
}
}

View File

@ -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',
}
}]
}
}

View File

@ -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]
}]
}
}
}

View File

@ -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()
}]
}
}

View File

@ -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,
}))
}
}

View File

@ -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' },
]
}
}

View File

@ -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
}]
}
}