allow disabling plugins

This commit is contained in:
Eugene Pankov
2017-11-26 22:14:46 +01:00
parent 0c15f5033d
commit 0de12b6b38
17 changed files with 92 additions and 42 deletions

View File

@@ -5,7 +5,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
export function getRootModule (plugins: any[]) { export function getRootModule (plugins: any[]) {
let imports = [ let imports = [
BrowserModule, BrowserModule,
...(plugins.map(x => x.default.forRoot ? x.default.forRoot() : x.default)), ...plugins,
NgbModule.forRoot(), NgbModule.forRoot(),
] ]
let bootstrap = [ let bootstrap = [

View File

@@ -30,6 +30,7 @@ async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgM
(document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%' (document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%'
}) })
let module = getRootModule(pluginsModules) let module = getRootModule(pluginsModules)
window['rootModule'] = module
return await platformBrowserDynamic().bootstrapModule(module) return await platformBrowserDynamic().bootstrapModule(module)
} }

View File

@@ -82,7 +82,7 @@ nodeRequire('module').prototype.require = function (query) {
export async function findPlugins (): Promise<IPluginInfo[]> { export async function findPlugins (): Promise<IPluginInfo[]> {
let paths = nodeModule.globalPaths let paths = nodeModule.globalPaths
let foundPlugins: IPluginInfo[] = [] let foundPlugins: IPluginInfo[] = []
let candidateLocations: { pluginDir: string, pluginName: string }[] = [] let candidateLocations: { pluginDir: string, packageName: string }[] = []
for (let pluginDir of paths) { for (let pluginDir of paths) {
pluginDir = normalizePath(pluginDir) pluginDir = normalizePath(pluginDir)
@@ -93,24 +93,26 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
if (await fs.exists(path.join(pluginDir, 'package.json'))) { if (await fs.exists(path.join(pluginDir, 'package.json'))) {
candidateLocations.push({ candidateLocations.push({
pluginDir: path.dirname(pluginDir), pluginDir: path.dirname(pluginDir),
pluginName: path.basename(pluginDir) packageName: path.basename(pluginDir)
}) })
} }
for (let pluginName of pluginNames) { for (let packageName of pluginNames) {
candidateLocations.push({ pluginDir, pluginName }) candidateLocations.push({ pluginDir, packageName })
} }
} }
for (let { pluginDir, pluginName } of candidateLocations) { for (let { pluginDir, packageName } of candidateLocations) {
let pluginPath = path.join(pluginDir, pluginName) let pluginPath = path.join(pluginDir, packageName)
let infoPath = path.join(pluginPath, 'package.json') let infoPath = path.join(pluginPath, 'package.json')
if (!await fs.exists(infoPath)) { if (!await fs.exists(infoPath)) {
continue continue
} }
if (foundPlugins.some(x => x.name === pluginName.substring('terminus-'.length))) { let name = packageName.substring('terminus-'.length)
console.info(`Plugin ${pluginName} already exists, overriding`)
foundPlugins = foundPlugins.filter(x => x.name !== pluginName.substring('terminus-'.length)) if (foundPlugins.some(x => x.name === name)) {
console.info(`Plugin ${packageName} already exists, overriding`)
foundPlugins = foundPlugins.filter(x => x.name !== name)
} }
try { try {
@@ -121,8 +123,8 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
let author = info.author let author = info.author
author = author.name || author author = author.name || author
foundPlugins.push({ foundPlugins.push({
name: pluginName.substring('terminus-'.length), name: name,
packageName: pluginName, packageName: packageName,
isBuiltin: pluginDir === builtinPluginsPath, isBuiltin: pluginDir === builtinPluginsPath,
version: info.version, version: info.version,
description: info.description, description: info.description,
@@ -131,7 +133,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
info, info,
}) })
} catch (error) { } catch (error) {
console.error('Cannot load package info for', pluginName) console.error('Cannot load package info for', packageName)
} }
} }
@@ -147,7 +149,10 @@ export async function loadPlugins (foundPlugins: IPluginInfo[], progress: Progre
console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`) console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
progress(index, foundPlugins.length) progress(index, foundPlugins.length)
try { try {
let pluginModule = nodeRequire(foundPlugin.path) let packageModule = nodeRequire(foundPlugin.path)
let pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
pluginModule['pluginName'] = foundPlugin.name
pluginModule['bootstrap'] = packageModule.bootstrap
plugins.push(pluginModule) plugins.push(pluginModule)
} catch (error) { } catch (error) {
console.error(`Could not load ${foundPlugin.name}:`, error) console.error(`Could not load ${foundPlugin.name}:`, error)

View File

@@ -170,7 +170,7 @@ export class AppRootComponent {
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] { private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
let buttons: IToolbarButton[] = [] let buttons: IToolbarButton[] = []
this.toolbarButtonProviders.forEach((provider) => { this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
buttons = buttons.concat(provider.provide()) buttons = buttons.concat(provider.provide())
}) })
return buttons return buttons

View File

@@ -1,6 +1,7 @@
import * as os from 'os' import * as os from 'os'
import { Component, Inject } from '@angular/core' import { Component, Inject } from '@angular/core'
import { ElectronService } from '../services/electron.service' import { ElectronService } from '../services/electron.service'
import { ConfigService } from '../services/config.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api' import { IToolbarButton, ToolbarButtonProvider } from '../api'
@Component({ @Component({
@@ -13,13 +14,14 @@ export class StartPageComponent {
constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService,
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[], @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
) { ) {
this.version = electron.app.getVersion() this.version = electron.app.getVersion()
} }
getButtons (): IToolbarButton[] { getButtons (): IToolbarButton[] {
return this.toolbarButtonProviders return this.config.enabledServices(this.toolbarButtonProviders)
.map(provider => provider.provide()) .map(provider => provider.provide())
.reduce((a, b) => a.concat(b)) .reduce((a, b) => a.concat(b))
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))

View File

@@ -6,3 +6,4 @@ appearance:
theme: Standard theme: Standard
frame: thin frame: thin
css: '/* * { color: blue !important; } */' css: '/* * { color: blue !important; } */'
pluginBlacklist: []

View File

@@ -6,6 +6,7 @@ import { Injectable, Inject } from '@angular/core'
import { ConfigProvider } from '../api/configProvider' import { ConfigProvider } from '../api/configProvider'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service' import { HostAppService } from './hostApp.service'
import * as Reflect from 'core-js/es7/reflect'
const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s }) const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s })
@@ -57,6 +58,7 @@ export class ConfigService {
private _store: any private _store: any
private path: string private path: string
private defaults: any private defaults: any
private servicesCache: { [id: string]: Function[] } = null
constructor ( constructor (
electron: ElectronService, electron: ElectronService,
@@ -98,4 +100,28 @@ export class ConfigService {
requestRestart (): void { requestRestart (): void {
this.restartRequested = true this.restartRequested = true
} }
enabledServices<T> (services: T[]): T[] {
if (!this.servicesCache) {
this.servicesCache = {}
let ngModule = Reflect.getMetadata('annotations', window['rootModule'])[0]
for (let imp of ngModule.imports) {
let module = imp['module'] || imp
let annotations = Reflect.getMetadata('annotations', module)
if (annotations) {
this.servicesCache[module['pluginName']] = annotations[0].providers.map(provider => {
return provider['useClass'] || provider
})
}
}
}
return services.filter(service => {
for (let pluginName in this.servicesCache) {
if (this.servicesCache[pluginName].includes(service.constructor)) {
return !this.store.pluginBlacklist.includes(pluginName)
}
}
return true
})
}
} }

View File

@@ -42,7 +42,7 @@ export class HotkeysService {
} }
}) })
}) })
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.hotkeyDescriptions = this.config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.config.changed$.subscribe(() => { this.config.changed$.subscribe(() => {
this.registerGlobalHotkey() this.registerGlobalHotkey()
}) })

View File

@@ -3,6 +3,7 @@ import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service' import { Logger, LogService } from '../services/log.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { ConfigService } from '../services/config.service'
@Injectable() @Injectable()
export class TabRecoveryService { export class TabRecoveryService {
@@ -11,6 +12,7 @@ export class TabRecoveryService {
constructor ( constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], @Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private app: AppService, private app: AppService,
private config: ConfigService,
log: LogService log: LogService
) { ) {
this.logger = log.create('tabRecovery') this.logger = log.create('tabRecovery')
@@ -31,7 +33,7 @@ export class TabRecoveryService {
if (window.localStorage.tabsRecovery) { if (window.localStorage.tabsRecovery) {
let tabs: RecoveredTab[] = [] let tabs: RecoveredTab[] = []
for (let token of JSON.parse(window.localStorage.tabsRecovery)) { for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.tabRecoveryProviders) { for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try { try {
let tab = await provider.recover(token) let tab = await provider.recover(token)
if (tab) { if (tab) {

View File

@@ -17,7 +17,7 @@ export class ThemesService {
} }
findTheme (name: string): Theme { findTheme (name: string): Theme {
return this.themes.find(x => x.name === name) return this.config.enabledServices(this.themes).find(x => x.name === name)
} }
findCurrentTheme (): Theme { findCurrentTheme (): Theme {

View File

@@ -10,34 +10,23 @@ h3 Installed
.list-group .list-group
ng-container(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"') ng-container(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
.list-group-item.flex-column.align-items-start(*ngIf='knownUpgrades[plugin.name]') .list-group-item.flex-column.align-items-start
.d-flex.w-100 .d-flex.w-100
.mr-auto.d-flex.flex-column .mr-auto.d-flex.flex-column
strong {{plugin.name}} strong {{plugin.name}}
small.text-muted.mb-0((click)='showPluginInfo(plugin)') {{plugin.description}} a.text-muted.mb-0((click)='showPluginInfo(plugin)')
small {{plugin.description}}
.d-flex.flex-column.align-items-end.mr-3 .d-flex.flex-column.align-items-end.mr-3
div {{plugin.version}} div {{plugin.version}}
small.text-muted {{plugin.author}} small.text-muted {{plugin.author}}
button.btn.btn-outline-primary( button.btn.btn-outline-primary(
*ngIf='npmInstalled', *ngIf='npmInstalled && knownUpgrades[plugin.name]',
(click)='upgradePlugin(plugin)', (click)='upgradePlugin(plugin)',
[disabled]='busy[plugin.name] != undefined' [disabled]='busy[plugin.name] != undefined'
) )
i.fa.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing') i.fa.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing') i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
span Upgrade ({{knownUpgrades[plugin.name].version}}) span Upgrade ({{knownUpgrades[plugin.name].version}})
ng-container(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
.list-group-item.flex-column.align-items-start(*ngIf='!knownUpgrades[plugin.name]')
.d-flex.w-100
.mr-auto.d-flex.flex-column
strong {{plugin.name}}
a.text-muted.mb-0((click)='showPluginInfo(plugin)')
small {{plugin.description}}
.d-flex.flex-column.align-items-end.mr-3
div {{plugin.version}}
small.text-muted {{plugin.author}}
i.fa.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official')
button.btn.btn-outline-danger( button.btn.btn-outline-danger(
(click)='uninstallPlugin(plugin)', (click)='uninstallPlugin(plugin)',
*ngIf='!plugin.isBuiltin && npmInstalled', *ngIf='!plugin.isBuiltin && npmInstalled',
@@ -45,6 +34,16 @@ h3 Installed
) )
i.fa.fa-fw.fa-trash-o(*ngIf='busy[plugin.name] != BusyState.Uninstalling') i.fa.fa-fw.fa-trash-o(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling') i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
button.btn.btn-outline-danger(
*ngIf='config.store.pluginBlacklist.includes(plugin.name)',
(click)='enablePlugin(plugin)'
)
i.fa.fa-fw.fa-play
button.btn.btn-outline-primary(
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
(click)='disablePlugin(plugin)'
)
i.fa.fa-fw.fa-pause
.text-center.mt-5(*ngIf='npmMissing') .text-center.mt-5(*ngIf='npmMissing')
h4 npm not installed h4 npm not installed

View File

@@ -105,4 +105,16 @@ export class PluginsSettingsTabComponent {
showPluginInfo (plugin: IPluginInfo) { showPluginInfo (plugin: IPluginInfo) {
this.electron.shell.openExternal('https://www.npmjs.com/package/' + plugin.packageName) this.electron.shell.openExternal('https://www.npmjs.com/package/' + plugin.packageName)
} }
enablePlugin (plugin: IPluginInfo) {
this.config.store.pluginBlacklist = this.config.store.pluginBlacklist.filter(x => x !== plugin.name)
this.config.save()
this.config.requestRestart()
}
disablePlugin (plugin: IPluginInfo) {
this.config.store.pluginBlacklist.push(plugin.name)
this.config.save()
this.config.requestRestart()
}
} }

View File

@@ -27,10 +27,12 @@ export class SettingsTabComponent extends BaseTabComponent {
@Inject(Theme) public themes: Theme[], @Inject(Theme) public themes: Theme[],
) { ) {
super() super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.title = 'Settings' this.title = 'Settings'
this.scrollable = true this.scrollable = true
this.screens = this.docking.getScreens() this.screens = this.docking.getScreens()
this.settingsProviders = config.enabledServices(this.settingsProviders)
this.themes = config.enabledServices(this.themes)
} }
getRecoveryToken (): any { getRecoveryToken (): any {

View File

@@ -27,7 +27,7 @@ export class TerminalSettingsTabComponent {
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[], @Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
@Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[], @Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[],
) { ) {
this.persistenceProviders = persistenceProviders.filter(x => x.isAvailable()) this.persistenceProviders = this.config.enabledServices(persistenceProviders).filter(x => x.isAvailable())
} }
async ngOnInit () { async ngOnInit () {
@@ -46,8 +46,8 @@ export class TerminalSettingsTabComponent {
this.fonts.sort() this.fonts.sort()
}) })
} }
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b)) this.colorSchemes = (await Promise.all(this.config.enabledServices(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)) this.shells = (await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))).reduce((a, b) => a.concat(b))
} }
fontAutocomplete = (text$: Observable<string>) => { fontAutocomplete = (text$: Observable<string>) => {

View File

@@ -125,7 +125,7 @@ export class TerminalTabComponent extends BaseTabComponent {
}) })
this.hterm = new hterm.hterm.Terminal() this.hterm = new hterm.hterm.Terminal()
this.decorators.forEach((decorator) => { this.config.enabledServices(this.decorators).forEach((decorator) => {
decorator.attach(this) decorator.attach(this)
}) })
@@ -406,7 +406,7 @@ export class TerminalTabComponent extends BaseTabComponent {
} }
ngOnDestroy () { ngOnDestroy () {
this.decorators.forEach(decorator => { this.config.enabledServices(this.decorators).forEach(decorator => {
decorator.detach(this) decorator.detach(this)
}) })
this.hotkeysSubscription.unsubscribe() this.hotkeysSubscription.unsubscribe()

View File

@@ -202,7 +202,7 @@ export class SessionsService {
) { ) {
nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any) nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
this.logger = log.create('sessions') this.logger = log.create('sessions')
this.persistenceProviders = this.persistenceProviders.filter(x => x.isAvailable()) this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
} }
async prepareNewSession (options: SessionOptions): Promise<SessionOptions> { async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {

View File

@@ -27,7 +27,7 @@ export class TerminalService {
async reloadShells () { async reloadShells () {
this.shells$ = new AsyncSubject<IShell[]>() this.shells$ = new AsyncSubject<IShell[]>()
let shellLists = await Promise.all(this.shellProviders.map(x => x.provide())) let shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
this.shells$.next(shellLists.reduce((a, b) => a.concat(b))) this.shells$.next(shellLists.reduce((a, b) => a.concat(b)))
this.shells$.complete() this.shells$.complete()
} }