1
1
mirror of https://github.com/Eugeny/tabby.git synced 2024-12-22 18:11:43 +03:00
This commit is contained in:
Eugene Pankov 2017-03-24 22:24:12 +01:00
parent 3b47047052
commit 739750e8f0
15 changed files with 281 additions and 192 deletions

View File

@ -9,4 +9,4 @@ html
script(src='./preload.js') script(src='./preload.js')
script(src='./bundle.js', defer) script(src='./bundle.js', defer)
body(style='background: ; min-height: 100vh') body(style='background: ; min-height: 100vh')
app app-root

20
app/src/api.ts Normal file
View File

@ -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'

View File

@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms'
import { ToasterModule } from 'angular2-toaster' import { ToasterModule } from 'angular2-toaster'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { AppService } from 'services/app'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp' import { HostAppService } from 'services/hostApp'
@ -12,11 +13,11 @@ import { LogService } from 'services/log'
import { HotkeysService } from 'services/hotkeys' import { HotkeysService } from 'services/hotkeys'
import { ModalService } from 'services/modal' import { ModalService } from 'services/modal'
import { NotifyService } from 'services/notify' import { NotifyService } from 'services/notify'
import { PluginDispatcherService } from 'services/pluginDispatcher' import { PluginsService } from 'services/plugins'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
import { AppComponent } from 'components/app' import { AppRootComponent } from 'components/appRoot'
import { CheckboxComponent } from 'components/checkbox' import { CheckboxComponent } from 'components/checkbox'
import { TabBodyComponent } from 'components/tabBody' import { TabBodyComponent } from 'components/tabBody'
import { TabHeaderComponent } from 'components/tabHeader' import { TabHeaderComponent } from 'components/tabHeader'
@ -37,6 +38,7 @@ let plugins = [
NgbModule.forRoot(), NgbModule.forRoot(),
].concat(plugins), ].concat(plugins),
providers: [ providers: [
AppService,
ConfigService, ConfigService,
DockingService, DockingService,
ElectronService, ElectronService,
@ -45,24 +47,24 @@ let plugins = [
LogService, LogService,
ModalService, ModalService,
NotifyService, NotifyService,
PluginDispatcherService, PluginsService,
QuitterService, QuitterService,
], ],
entryComponents: [ entryComponents: [
], ],
declarations: [ declarations: [
AppComponent, AppRootComponent,
CheckboxComponent, CheckboxComponent,
TabBodyComponent, TabBodyComponent,
TabHeaderComponent, TabHeaderComponent,
TitleBarComponent, TitleBarComponent,
], ],
bootstrap: [ bootstrap: [
AppComponent, AppRootComponent,
] ]
}) })
export class AppModule { export class AppModule {
constructor (pluginDispatcher: PluginDispatcherService) { constructor () {
pluginDispatcher.register(require('./plugin.hyperlinks').default) //pluginDispatcher.register(require('./plugin.hyperlinks').default)
} }
} }

View File

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

View File

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

View File

@ -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 { ToasterConfig } from 'angular2-toaster'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
@ -8,10 +8,9 @@ import { LogService } from 'services/log'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
// import { SessionsService } from 'services/sessions' import { PluginsService } from 'services/plugins'
import { PluginDispatcherService } from 'services/pluginDispatcher'
import { Tab } from 'models/tab' import { AppService, IToolbarButton, IToolbarButtonProvider, ToolbarButtonProviderType } from 'api'
import 'angular2-toaster/lib/toaster.css' import 'angular2-toaster/lib/toaster.css'
import 'global.less' import 'global.less'
@ -19,9 +18,9 @@ import 'theme.scss'
@Component({ @Component({
selector: 'app', selector: 'app-root',
template: require('./app.pug'), template: require('./appRoot.pug'),
styles: [require('./app.less')], styles: [require('./appRoot.less')],
animations: [ animations: [
trigger('animateTab', [ trigger('animateTab', [
state('in', style({ state('in', style({
@ -41,27 +40,24 @@ import 'theme.scss'
]) ])
] ]
}) })
export class AppComponent { export class AppRootComponent {
toasterConfig: ToasterConfig toasterConfig: ToasterConfig
@Input() tabs: Tab[] = []
@Input() activeTab: Tab
lastTabIndex = 0
constructor( constructor(
// private sessions: SessionsService,
private docking: DockingService, private docking: DockingService,
private electron: ElectronService, private electron: ElectronService,
public hostApp: HostAppService, public hostApp: HostAppService,
public hotkeys: HotkeysService, public hotkeys: HotkeysService,
public config: ConfigService, public config: ConfigService,
private pluginDispatcher: PluginDispatcherService, private plugins: PluginsService,
public app: AppService,
log: LogService, log: LogService,
_quitter: QuitterService, _quitter: QuitterService,
) { ) {
console.timeStamp('AppComponent ctor') console.timeStamp('AppComponent ctor')
let logger = log.create('main') let logger = log.create('main')
logger.info('ELEMENTS client', electron.app.getVersion()) logger.info('v', electron.app.getVersion())
this.toasterConfig = new ToasterConfig({ this.toasterConfig = new ToasterConfig({
mouseoverTimerStop: true, mouseoverTimerStop: true,
@ -71,42 +67,26 @@ export class AppComponent {
this.hotkeys.matchedHotkey.subscribe((hotkey) => { this.hotkeys.matchedHotkey.subscribe((hotkey) => {
if (hotkey == 'new-tab') { if (hotkey == 'new-tab') {
this.newTab() // TODO this.newTab()
} }
if (hotkey.startsWith('tab-')) { if (hotkey.startsWith('tab-')) {
let index = parseInt(hotkey.split('-')[1]) let index = parseInt(hotkey.split('-')[1])
if (index <= this.tabs.length) { if (index <= this.app.tabs.length) {
this.selectTab(this.tabs[index - 1]) this.app.selectTab(this.app.tabs[index - 1])
} }
} }
if (this.activeTab) { if (this.app.activeTab) {
if (hotkey == 'close-tab') { if (hotkey == 'close-tab') {
this.closeTab(this.activeTab) this.app.closeTab(this.app.activeTab)
} }
if (hotkey == 'toggle-last-tab') { if (hotkey == 'toggle-last-tab') {
this.toggleLastTab() this.app.toggleLastTab()
} }
if (hotkey == 'next-tab') { if (hotkey == 'next-tab') {
this.nextTab() this.app.nextTab()
} }
if (hotkey == 'previous-tab') { if (hotkey == 'previous-tab') {
this.previousTab() this.app.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])
}
} }
} }
}) })
@ -145,57 +125,15 @@ export class AppComponent {
}) })
} }
newTab () { getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
const tab = this.pluginDispatcher.temp2('zsh') let buttons: IToolbarButton[] = []
this.tabs.push(tab) this.plugins.getAll<IToolbarButtonProvider>(ToolbarButtonProviderType)
this.selectTab(tab) .forEach((provider) => {
} buttons = buttons.concat(provider.provide())
})
selectTab (tab) { return buttons
if (this.tabs.includes(this.activeTab)) { .filter((button) => (button.weight > 0) === aboveZero)
this.lastTabIndex = this.tabs.indexOf(this.activeTab) .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
} 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])
}
} }
ngOnInit () { 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)
}
} }

66
app/src/services/app.ts Normal file
View File

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

View File

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

View File

@ -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<T extends IPlugin> (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) => <T>(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)
}
})
}
}

View File

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

View File

@ -8,9 +8,11 @@ import { HotkeyDisplayComponent } from './components/hotkeyDisplay'
import { HotkeyHintComponent } from './components/hotkeyHint' import { HotkeyHintComponent } from './components/hotkeyHint'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal' import { HotkeyInputModalComponent } from './components/hotkeyInputModal'
import { SettingsPaneComponent } from './components/settingsPane' import { SettingsPaneComponent } from './components/settingsPane'
import { PluginDispatcherService } from 'services/pluginDispatcher'
import { SettingsTab } from './tab' import { PluginsService, ToolbarButtonProviderType } from 'api'
import { ButtonProvider } from './buttonProvider'
@NgModule({ @NgModule({
imports: [ imports: [
@ -19,6 +21,7 @@ import { SettingsTab } from './tab'
NgbModule, NgbModule,
], ],
providers: [ providers: [
ButtonProvider,
], ],
entryComponents: [ entryComponents: [
HotkeyInputModalComponent, HotkeyInputModalComponent,
@ -33,8 +36,8 @@ import { SettingsTab } from './tab'
], ],
}) })
class SettingsModule { class SettingsModule {
constructor (pluginDispatcher: PluginDispatcherService) { constructor (plugins: PluginsService, buttonProvider: ButtonProvider) {
pluginDispatcher.temp = SettingsTab plugins.register(ToolbarButtonProviderType, buttonProvider, 1)
} }
} }

View File

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

View File

@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core' import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { PluginDispatcherService } from 'services/pluginDispatcher' import { PluginsService } from 'services/plugins'
import { BaseTabComponent } from 'components/baseTab' import { BaseTabComponent } from 'components/baseTab'
import { TerminalTab } from '../tab' import { TerminalTab } from '../tab'
@ -27,7 +27,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
private zone: NgZone, private zone: NgZone,
private elementRef: ElementRef, private elementRef: ElementRef,
public config: ConfigService, public config: ConfigService,
private pluginDispatcher: PluginDispatcherService, private plugins: PluginsService,
) { ) {
super() super()
this.startupTime = performance.now() this.startupTime = performance.now()
@ -42,7 +42,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
}) })
this.terminal = new hterm.hterm.Terminal() 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.terminal.setWindowTitle = (title) => {
this.zone.run(() => { this.zone.run(() => {
this.title = title this.title = title
@ -77,7 +77,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
} }
this.terminal.decorate(this.elementRef.nativeElement) this.terminal.decorate(this.elementRef.nativeElement)
this.configure() this.configure()
this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal }) //this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal })
} }
configure () { configure () {

View File

@ -2,11 +2,11 @@ import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { PluginsService, ToolbarButtonProviderType } from 'api'
import { TerminalTabComponent } from './components/terminalTab' import { TerminalTabComponent } from './components/terminalTab'
import { SessionsService } from './services/sessions' import { SessionsService } from './services/sessions'
import { TerminalTab } from './tab' import { ButtonProvider } from './buttonProvider'
import { PluginDispatcherService } from 'services/pluginDispatcher'
@NgModule({ @NgModule({
@ -15,6 +15,7 @@ import { PluginDispatcherService } from 'services/pluginDispatcher'
FormsModule, FormsModule,
], ],
providers: [ providers: [
ButtonProvider,
SessionsService, SessionsService,
], ],
entryComponents: [ entryComponents: [
@ -25,11 +26,8 @@ import { PluginDispatcherService } from 'services/pluginDispatcher'
], ],
}) })
class TerminalModule { class TerminalModule {
constructor (pluginDispatcher: PluginDispatcherService, sessions: SessionsService) { constructor (plugins: PluginsService, buttonProvider: ButtonProvider) {
pluginDispatcher.temp2 = (command) => { plugins.register(ToolbarButtonProviderType, buttonProvider)
let session = sessions.createNewSession({ command }))
return new TerminalTab(session)
}
} }
} }