diff --git a/app/index.pug b/app/index.pug index 450bb478..d927c3a8 100644 --- a/app/index.pug +++ b/app/index.pug @@ -9,4 +9,4 @@ html script(src='./preload.js') script(src='./bundle.js', defer) body(style='background: ; min-height: 100vh') - app + app-root diff --git a/app/src/api.ts b/app/src/api.ts new file mode 100644 index 00000000..b41ab60f --- /dev/null +++ b/app/src/api.ts @@ -0,0 +1,20 @@ +export { AppService } from 'services/app' +export { PluginsService } from 'services/plugins' +export { Tab } from 'models/tab' + +export interface IPlugin { + +} + +export interface IToolbarButton { + icon: string + title: string + weight?: number + click: () => void +} + +export interface IToolbarButtonProvider { + provide (): IToolbarButton[] +} + +export const ToolbarButtonProviderType = 'app:toolbar-button-provider' diff --git a/app/src/app.module.ts b/app/src/app.module.ts index c3be988d..7db40aac 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms' import { ToasterModule } from 'angular2-toaster' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' +import { AppService } from 'services/app' import { ConfigService } from 'services/config' import { ElectronService } from 'services/electron' import { HostAppService } from 'services/hostApp' @@ -12,11 +13,11 @@ import { LogService } from 'services/log' import { HotkeysService } from 'services/hotkeys' import { ModalService } from 'services/modal' import { NotifyService } from 'services/notify' -import { PluginDispatcherService } from 'services/pluginDispatcher' +import { PluginsService } from 'services/plugins' import { QuitterService } from 'services/quitter' import { DockingService } from 'services/docking' -import { AppComponent } from 'components/app' +import { AppRootComponent } from 'components/appRoot' import { CheckboxComponent } from 'components/checkbox' import { TabBodyComponent } from 'components/tabBody' import { TabHeaderComponent } from 'components/tabHeader' @@ -37,6 +38,7 @@ let plugins = [ NgbModule.forRoot(), ].concat(plugins), providers: [ + AppService, ConfigService, DockingService, ElectronService, @@ -45,24 +47,24 @@ let plugins = [ LogService, ModalService, NotifyService, - PluginDispatcherService, + PluginsService, QuitterService, ], entryComponents: [ ], declarations: [ - AppComponent, + AppRootComponent, CheckboxComponent, TabBodyComponent, TabHeaderComponent, TitleBarComponent, ], bootstrap: [ - AppComponent, + AppRootComponent, ] }) export class AppModule { - constructor (pluginDispatcher: PluginDispatcherService) { - pluginDispatcher.register(require('./plugin.hyperlinks').default) + constructor () { + //pluginDispatcher.register(require('./plugin.hyperlinks').default) } } diff --git a/app/src/components/app.pug b/app/src/components/app.pug deleted file mode 100644 index f8429b2e..00000000 --- a/app/src/components/app.pug +++ /dev/null @@ -1,35 +0,0 @@ -title-bar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"') - -.spacer - -.tabs(class='active-tab-{{tabs.indexOf(activeTab)}}') - button.btn.btn-secondary.btn-new-tab((click)='newTab()') - i.fa.fa-plus - tab-header( - *ngFor='let tab of tabs; let idx = index; trackBy: tab?.id', - [index]='idx', - [model]='tab', - [active]='tab == activeTab', - [hasActivity]='tab.hasActivity', - @animateTab, - (click)='selectTab(tab)', - (closeClicked)='closeTab(tab)', - ) - button.btn.btn-secondary.btn-settings((click)='showSettings()') - i.fa.fa-cog - -.tabs-content - tab-body( - *ngFor='let tab of tabs; trackBy: tab?.id', - [active]='tab == activeTab', - [model]='tab', - [class.scrollable]='tab.scrollable', - ) - -// TODO -//hotkey-hint - -toaster-container([toasterconfig]="toasterconfig") -template(ngbModalContainer) - -div.window-resizer.window-resizer-tl diff --git a/app/src/components/app.less b/app/src/components/appRoot.less similarity index 100% rename from app/src/components/app.less rename to app/src/components/appRoot.less diff --git a/app/src/components/appRoot.pug b/app/src/components/appRoot.pug new file mode 100644 index 00000000..24d2caa9 --- /dev/null +++ b/app/src/components/appRoot.pug @@ -0,0 +1,43 @@ +title-bar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"') + +.spacer + +.tabs(class='active-tab-{{app.tabs.indexOf(app.activeTab)}}') + button.btn.btn-secondary( + *ngFor='let button of getToolbarButtons(false)', + [title]='button.title', + (click)='button.click()', + ) + i.fa([class]='"fa fa-" + button.icon') + tab-header( + *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', + [index]='idx', + [model]='tab', + [active]='tab == app.activeTab', + [hasActivity]='tab.hasActivity', + @animateTab, + (click)='app.selectTab(tab)', + (closeClicked)='app.closeTab(tab)', + ) + button.btn.btn-secondary( + *ngFor='let button of getToolbarButtons(true)', + [title]='button.title', + (click)='button.click()', + ) + i.fa([class]='"fa fa-" + button.icon') + +.tabs-content + tab-body( + *ngFor='let tab of app.tabs; trackBy: tab?.id', + [active]='tab == app.activeTab', + [model]='tab', + [class.scrollable]='tab.scrollable', + ) + +// TODO +//hotkey-hint + +toaster-container([toasterconfig]="toasterconfig") +template(ngbModalContainer) + +div.window-resizer.window-resizer-tl diff --git a/app/src/components/app.ts b/app/src/components/appRoot.ts similarity index 52% rename from app/src/components/app.ts rename to app/src/components/appRoot.ts index 03c55a22..af94eae1 100644 --- a/app/src/components/app.ts +++ b/app/src/components/appRoot.ts @@ -1,4 +1,4 @@ -import { Component, Input, trigger, style, animate, transition, state } from '@angular/core' +import { Component, trigger, style, animate, transition, state } from '@angular/core' import { ToasterConfig } from 'angular2-toaster' import { ElectronService } from 'services/electron' @@ -8,10 +8,9 @@ import { LogService } from 'services/log' import { QuitterService } from 'services/quitter' import { ConfigService } from 'services/config' import { DockingService } from 'services/docking' -// import { SessionsService } from 'services/sessions' -import { PluginDispatcherService } from 'services/pluginDispatcher' +import { PluginsService } from 'services/plugins' -import { Tab } from 'models/tab' +import { AppService, IToolbarButton, IToolbarButtonProvider, ToolbarButtonProviderType } from 'api' import 'angular2-toaster/lib/toaster.css' import 'global.less' @@ -19,9 +18,9 @@ import 'theme.scss' @Component({ - selector: 'app', - template: require('./app.pug'), - styles: [require('./app.less')], + selector: 'app-root', + template: require('./appRoot.pug'), + styles: [require('./appRoot.less')], animations: [ trigger('animateTab', [ state('in', style({ @@ -41,27 +40,24 @@ import 'theme.scss' ]) ] }) -export class AppComponent { +export class AppRootComponent { toasterConfig: ToasterConfig - @Input() tabs: Tab[] = [] - @Input() activeTab: Tab - lastTabIndex = 0 constructor( -// private sessions: SessionsService, private docking: DockingService, private electron: ElectronService, public hostApp: HostAppService, public hotkeys: HotkeysService, public config: ConfigService, - private pluginDispatcher: PluginDispatcherService, + private plugins: PluginsService, + public app: AppService, log: LogService, _quitter: QuitterService, ) { console.timeStamp('AppComponent ctor') let logger = log.create('main') - logger.info('ELEMENTS client', electron.app.getVersion()) + logger.info('v', electron.app.getVersion()) this.toasterConfig = new ToasterConfig({ mouseoverTimerStop: true, @@ -71,42 +67,26 @@ export class AppComponent { this.hotkeys.matchedHotkey.subscribe((hotkey) => { if (hotkey == 'new-tab') { - this.newTab() + // TODO this.newTab() } if (hotkey.startsWith('tab-')) { let index = parseInt(hotkey.split('-')[1]) - if (index <= this.tabs.length) { - this.selectTab(this.tabs[index - 1]) + if (index <= this.app.tabs.length) { + this.app.selectTab(this.app.tabs[index - 1]) } } - if (this.activeTab) { + if (this.app.activeTab) { if (hotkey == 'close-tab') { - this.closeTab(this.activeTab) + this.app.closeTab(this.app.activeTab) } if (hotkey == 'toggle-last-tab') { - this.toggleLastTab() + this.app.toggleLastTab() } if (hotkey == 'next-tab') { - this.nextTab() + this.app.nextTab() } if (hotkey == 'previous-tab') { - this.previousTab() - } - } - }) - - this.hotkeys.key.subscribe((key) => { - if (key.event == 'keydown') { - if (key.alt && key.key >= '1' && key.key <= '9') { - let index = key.key.charCodeAt(0) - '0'.charCodeAt(0) - 1 - if (index < this.tabs.length) { - this.selectTab(this.tabs[index]) - } - } - if (key.alt && key.key == '0') { - if (this.tabs.length >= 10) { - this.selectTab(this.tabs[9]) - } + this.app.previousTab() } } }) @@ -145,57 +125,15 @@ export class AppComponent { }) } - newTab () { - const tab = this.pluginDispatcher.temp2('zsh') - this.tabs.push(tab) - this.selectTab(tab) - } - - selectTab (tab) { - if (this.tabs.includes(this.activeTab)) { - this.lastTabIndex = this.tabs.indexOf(this.activeTab) - } else { - this.lastTabIndex = null - } - if (this.activeTab) { - this.activeTab.hasActivity = false - this.activeTab.blurred.emit() - } - this.activeTab = tab - this.activeTab.focused.emit() - } - - toggleLastTab () { - if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) { - this.lastTabIndex = 0 - } - this.selectTab(this.tabs[this.lastTabIndex]) - } - - nextTab () { - let tabIndex = this.tabs.indexOf(this.activeTab) - if (tabIndex < this.tabs.length - 1) { - this.selectTab(this.tabs[tabIndex + 1]) - } - } - - previousTab () { - let tabIndex = this.tabs.indexOf(this.activeTab) - if (tabIndex > 0) { - this.selectTab(this.tabs[tabIndex - 1]) - } - } - - closeTab (tab) { - tab.destroy() - /* if (tab.session) { - this.sessions.destroySession(tab.session) - } */ - let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1) - this.tabs = this.tabs.filter((x) => x != tab) - if (tab == this.activeTab) { - this.selectTab(this.tabs[newIndex]) - } + getToolbarButtons (aboveZero: boolean): IToolbarButton[] { + let buttons: IToolbarButton[] = [] + this.plugins.getAll(ToolbarButtonProviderType) + .forEach((provider) => { + buttons = buttons.concat(provider.provide()) + }) + return buttons + .filter((button) => (button.weight > 0) === aboveZero) + .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) } ngOnInit () { @@ -212,14 +150,4 @@ export class AppComponent { }) */ } - - showSettings() { - const SettingsTab = this.pluginDispatcher.temp - let settingsTab = this.tabs.find((x) => x instanceof SettingsTab) - if (!settingsTab) { - settingsTab = new SettingsTab() - this.tabs.push(settingsTab) - } - this.selectTab(settingsTab) - } } diff --git a/app/src/services/app.ts b/app/src/services/app.ts new file mode 100644 index 00000000..1996edf6 --- /dev/null +++ b/app/src/services/app.ts @@ -0,0 +1,66 @@ +import { EventEmitter, Injectable } from '@angular/core' +import { Tab } from 'models/tab' + + +@Injectable() +export class AppService { + tabs: Tab[] = [] + activeTab: Tab + lastTabIndex = 0 + + constructor () { + + } + + openTab (tab: Tab): void { + this.tabs.push(tab) + this.selectTab(tab) + } + + selectTab (tab) { + if (this.tabs.includes(this.activeTab)) { + this.lastTabIndex = this.tabs.indexOf(this.activeTab) + } else { + this.lastTabIndex = null + } + if (this.activeTab) { + this.activeTab.hasActivity = false + this.activeTab.blurred.emit() + } + this.activeTab = tab + this.activeTab.focused.emit() + } + + toggleLastTab () { + if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) { + this.lastTabIndex = 0 + } + this.selectTab(this.tabs[this.lastTabIndex]) + } + + nextTab () { + let tabIndex = this.tabs.indexOf(this.activeTab) + if (tabIndex < this.tabs.length - 1) { + this.selectTab(this.tabs[tabIndex + 1]) + } + } + + previousTab () { + let tabIndex = this.tabs.indexOf(this.activeTab) + if (tabIndex > 0) { + this.selectTab(this.tabs[tabIndex - 1]) + } + } + + closeTab (tab) { + tab.destroy() + /* if (tab.session) { + this.sessions.destroySession(tab.session) + } */ + let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1) + this.tabs = this.tabs.filter((x) => x != tab) + if (tab == this.activeTab) { + this.selectTab(this.tabs[newIndex]) + } + } +} diff --git a/app/src/services/pluginDispatcher.ts b/app/src/services/pluginDispatcher.ts deleted file mode 100644 index ccc884de..00000000 --- a/app/src/services/pluginDispatcher.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core' -import { ConfigService } from 'services/config' -import { ElectronService } from 'services/electron' - - -@Injectable() -export class PluginDispatcherService { - plugins = [] - temp: any - temp2: any - - constructor ( - private config: ConfigService, - private electron: ElectronService, - ) { - } - - register (plugin) { - if (!this.plugins.includes(plugin)) { - this.plugins.push(new plugin({ - config: this.config, - electron: this.electron, - })) - } - } - - emit (event: string, parameters: any) { - this.plugins.forEach((plugin) => { - if (plugin[event]) { - plugin[event].bind(plugin)(parameters) - } - }) - } -} diff --git a/app/src/services/plugins.ts b/app/src/services/plugins.ts new file mode 100644 index 00000000..8f970671 --- /dev/null +++ b/app/src/services/plugins.ts @@ -0,0 +1,45 @@ +import { IPlugin } from 'api' +import { Injectable } from '@angular/core' + + +interface IPluginEntry { + plugin: IPlugin + weight: number +} + +@Injectable() +export class PluginsService { + plugins: {[type: string]: IPluginEntry[]} = {} + + constructor ( + ) { + } + + register (type: string, plugin: IPlugin, weight = 0): void { + if (!this.plugins[type]) { + this.plugins[type] = [] + } + this.plugins[type].push({ plugin, weight }) + } + + getAll (type: string): T[] { + let plugins = this.plugins[type] || [] + plugins = plugins.sort((a: IPluginEntry, b: IPluginEntry) => { + if (a.weight < b.weight) { + return -1 + } else if (a.weight > b.weight) { + return 1 + } + return 0 + }) + return plugins.map((x) => (x.plugin)) + } + + emit (type: string, event: string, parameters: any[]) { + (this.plugins[type] || []).forEach((entry) => { + if (entry.plugin[event]) { + entry.plugin[event].bind(entry.plugin)(parameters) + } + }) + } +} diff --git a/app/src/settings/buttonProvider.ts b/app/src/settings/buttonProvider.ts new file mode 100644 index 00000000..16c720a1 --- /dev/null +++ b/app/src/settings/buttonProvider.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core' +import { IToolbarButtonProvider, IToolbarButton, AppService } from 'api' +import { SettingsTab } from './tab' + + +@Injectable() +export class ButtonProvider implements IToolbarButtonProvider { + constructor ( + private app: AppService, + ) { } + + provide (): IToolbarButton[] { + return [{ + icon: 'cog', + title: 'Settings', + weight: 10, + click: () => { + let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTab) + if (settingsTab) { + this.app.selectTab(settingsTab) + } else { + this.app.openTab(new SettingsTab()) + } + } + }] + } +} diff --git a/app/src/settings/index.ts b/app/src/settings/index.ts index 3e4ace27..5994afe9 100644 --- a/app/src/settings/index.ts +++ b/app/src/settings/index.ts @@ -8,9 +8,11 @@ import { HotkeyDisplayComponent } from './components/hotkeyDisplay' import { HotkeyHintComponent } from './components/hotkeyHint' import { HotkeyInputModalComponent } from './components/hotkeyInputModal' import { SettingsPaneComponent } from './components/settingsPane' -import { PluginDispatcherService } from 'services/pluginDispatcher' -import { SettingsTab } from './tab' +import { PluginsService, ToolbarButtonProviderType } from 'api' + +import { ButtonProvider } from './buttonProvider' + @NgModule({ imports: [ @@ -19,6 +21,7 @@ import { SettingsTab } from './tab' NgbModule, ], providers: [ + ButtonProvider, ], entryComponents: [ HotkeyInputModalComponent, @@ -33,8 +36,8 @@ import { SettingsTab } from './tab' ], }) class SettingsModule { - constructor (pluginDispatcher: PluginDispatcherService) { - pluginDispatcher.temp = SettingsTab + constructor (plugins: PluginsService, buttonProvider: ButtonProvider) { + plugins.register(ToolbarButtonProviderType, buttonProvider, 1) } } diff --git a/app/src/terminal/buttonProvider.ts b/app/src/terminal/buttonProvider.ts new file mode 100644 index 00000000..05497ece --- /dev/null +++ b/app/src/terminal/buttonProvider.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core' +import { IToolbarButtonProvider, IToolbarButton, AppService } from 'api' +import { SessionsService } from './services/sessions' +import { TerminalTab } from './tab' + + +@Injectable() +export class ButtonProvider implements IToolbarButtonProvider { + constructor ( + private app: AppService, + private sessions: SessionsService, + ) { + + } + + provide (): IToolbarButton[] { + return [{ + icon: 'plus', + title: 'New terminal', + click: () => { + let session = this.sessions.createNewSession({ command: 'zsh' }) + this.app.openTab(new TerminalTab(session)) + } + }] + } +} diff --git a/app/src/terminal/components/terminalTab.ts b/app/src/terminal/components/terminalTab.ts index d721715c..913a4e38 100644 --- a/app/src/terminal/components/terminalTab.ts +++ b/app/src/terminal/components/terminalTab.ts @@ -2,7 +2,7 @@ import { Subscription } from 'rxjs' import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core' import { ConfigService } from 'services/config' -import { PluginDispatcherService } from 'services/pluginDispatcher' +import { PluginsService } from 'services/plugins' import { BaseTabComponent } from 'components/baseTab' import { TerminalTab } from '../tab' @@ -27,7 +27,7 @@ export class TerminalTabComponent extends BaseTabComponent { private zone: NgZone, private elementRef: ElementRef, public config: ConfigService, - private pluginDispatcher: PluginDispatcherService, + private plugins: PluginsService, ) { super() this.startupTime = performance.now() @@ -42,7 +42,7 @@ export class TerminalTabComponent extends BaseTabComponent { }) this.terminal = new hterm.hterm.Terminal() - this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal }) + //this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal }) this.terminal.setWindowTitle = (title) => { this.zone.run(() => { this.title = title @@ -77,7 +77,7 @@ export class TerminalTabComponent extends BaseTabComponent { } this.terminal.decorate(this.elementRef.nativeElement) this.configure() - this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal }) + //this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal }) } configure () { diff --git a/app/src/terminal/index.ts b/app/src/terminal/index.ts index e1af23dd..aa6bfcaf 100644 --- a/app/src/terminal/index.ts +++ b/app/src/terminal/index.ts @@ -2,11 +2,11 @@ import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { FormsModule } from '@angular/forms' +import { PluginsService, ToolbarButtonProviderType } from 'api' + import { TerminalTabComponent } from './components/terminalTab' import { SessionsService } from './services/sessions' -import { TerminalTab } from './tab' - -import { PluginDispatcherService } from 'services/pluginDispatcher' +import { ButtonProvider } from './buttonProvider' @NgModule({ @@ -15,6 +15,7 @@ import { PluginDispatcherService } from 'services/pluginDispatcher' FormsModule, ], providers: [ + ButtonProvider, SessionsService, ], entryComponents: [ @@ -25,11 +26,8 @@ import { PluginDispatcherService } from 'services/pluginDispatcher' ], }) class TerminalModule { - constructor (pluginDispatcher: PluginDispatcherService, sessions: SessionsService) { - pluginDispatcher.temp2 = (command) => { - let session = sessions.createNewSession({ command })) - return new TerminalTab(session) - } + constructor (plugins: PluginsService, buttonProvider: ButtonProvider) { + plugins.register(ToolbarButtonProviderType, buttonProvider) } }