This commit is contained in:
Eugene Pankov
2017-03-29 14:04:01 +02:00
parent 482343e383
commit cb7e1cd157
28 changed files with 295 additions and 703 deletions

View File

@@ -0,0 +1,4 @@
export abstract class ConfigProvider {
configStructure: any = {}
defaultConfigValues: any = {}
}

View File

@@ -0,0 +1,8 @@
export interface IHotkeyDescription {
id: string,
name: string,
}
export abstract class HotkeyProvider {
hotkeys: IHotkeyDescription[] = []
}

View File

@@ -1,7 +1,10 @@
export { Tab } from './tab'
export { TabRecoveryProvider } from './tabRecovery'
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider'
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
export { AppService } from 'services/app'
export { PluginsService } from 'services/plugins'
export { ElectronService } from 'services/electron'
export { HotkeysService } from 'services/hotkeys'

View File

@@ -10,7 +10,7 @@ import { ConfigService } from 'services/config'
import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp'
import { LogService } from 'services/log'
import { HotkeysService } from 'services/hotkeys'
import { HotkeysService, AppHotkeyProvider } from 'services/hotkeys'
import { ModalService } from 'services/modal'
import { NotifyService } from 'services/notify'
import { PluginsService } from 'services/plugins'
@@ -23,6 +23,8 @@ import { TabBodyComponent } from 'components/tabBody'
import { TabHeaderComponent } from 'components/tabHeader'
import { TitleBarComponent } from 'components/titleBar'
import { HotkeyProvider } from 'api/hotkeyProvider'
let plugins = [
require('./settings').default,
@@ -50,6 +52,7 @@ let plugins = [
NotifyService,
PluginsService,
QuitterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
],
entryComponents: [
],
@@ -65,7 +68,4 @@ let plugins = [
]
})
export class AppModule {
constructor () {
//pluginDispatcher.register(require('./plugin.hyperlinks').default)
}
}

View File

@@ -12,6 +12,7 @@ import { DockingService } from 'services/docking'
import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api'
import 'angular2-toaster/lib/toaster.css'
import 'overrides.scss'
import 'global.less'
import 'theme.scss'
@@ -65,9 +66,6 @@ export class AppRootComponent {
})
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
if (hotkey == 'new-tab') {
// TODO this.newTab()
}
if (hotkey.startsWith('tab-')) {
let index = parseInt(hotkey.split('-')[1])
if (index <= this.app.tabs.length) {

View File

@@ -66,24 +66,6 @@ body {
}
ngb-modal-backdrop {
// ngbmodalwindow has its own, properly animated backdrop
background: transparent !important;
}
ngb-modal-window.fade.in {
&.out {
opacity: 0;
.modal-dialog {
transform: translate(0, -25%);
}
}
}
.btn {
i + * {
margin-left: 5px;

9
app/src/overrides.scss Normal file
View File

@@ -0,0 +1,9 @@
ngb-tabset.vertical {
display: flex;
> .nav {
display: flex;
flex-direction: column;
flex: none;
}
}

View File

@@ -1,69 +1,54 @@
import * as yaml from 'js-yaml'
import * as path from 'path'
import * as fs from 'fs'
import { EventEmitter, Injectable } from '@angular/core'
import { EventEmitter, Injectable, Inject } from '@angular/core'
import { ElectronService } from 'services/electron'
import { ConfigProvider } from 'api/configProvider'
const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s })
const defaultConfigValues : IConfigData = require('../../defaultConfigValues.yaml')
const defaultConfigStructure : IConfigData = require('../../defaultConfigStructure.yaml')
export interface IAppearanceData {
useNativeFrame: boolean
font: string
fontSize: number
dock: string
dockScreen: string
dockFill: number
tabsOnTop: boolean
}
export interface ITerminalData {
bell: string|boolean
}
export interface IConfigData {
appearance?: IAppearanceData
hotkeys?: any
terminal?: ITerminalData
}
@Injectable()
export class ConfigService {
store: IConfigData
store: any
change = new EventEmitter()
restartRequested: boolean
private path: string
private configStructure: any = require('../../defaultConfigStructure.yaml')
private defaultConfigValues: any = require('../../defaultConfigValues.yaml')
constructor (
electron: ElectronService
electron: ElectronService,
@Inject(ConfigProvider) configProviders: ConfigProvider[],
) {
this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
this.configStructure = configProviders.map(x => x.configStructure).reduce(configMerge, this.configStructure)
this.defaultConfigValues = configProviders.map(x => x.defaultConfigValues).reduce(configMerge, this.defaultConfigValues)
this.load()
}
load () {
load (): void {
if (fs.existsSync(this.path)) {
this.store = configMerge(defaultConfigStructure, yaml.safeLoad(fs.readFileSync(this.path, 'utf8')))
this.store = configMerge(this.configStructure, yaml.safeLoad(fs.readFileSync(this.path, 'utf8')))
} else {
this.store = Object.assign({}, defaultConfigStructure)
this.store = Object.assign({}, this.configStructure)
}
}
save () {
save (): void {
fs.writeFileSync(this.path, yaml.safeDump(this.store), 'utf8')
this.emitChange()
}
full () : IConfigData {
return configMerge(defaultConfigValues, this.store)
full (): any {
return configMerge(this.defaultConfigValues, this.store)
}
emitChange () {
emitChange (): void {
this.change.emit()
}
requestRestart () {
requestRestart (): void {
this.restartRequested = true
}
}

View File

@@ -1,13 +1,10 @@
import { Injectable, NgZone, EventEmitter } from '@angular/core'
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config'
import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util'
import { IHotkeyDescription, HotkeyProvider } from 'api/hotkeyProvider'
const hterm = require('hterm-commonjs')
export interface HotkeyDescription {
id: string,
name: string,
}
export interface PartialHotkeyMatch {
id: string,
@@ -16,69 +13,6 @@ export interface PartialHotkeyMatch {
}
const KEY_TIMEOUT = 2000
const HOTKEYS: HotkeyDescription[] = [
{
id: 'new-tab',
name: 'New tab',
},
{
id: 'close-tab',
name: 'Close tab',
},
{
id: 'toggle-last-tab',
name: 'Toggle last tab',
},
{
id: 'next-tab',
name: 'Next tab',
},
{
id: 'previous-tab',
name: 'Previous tab',
},
{
id: 'tab-1',
name: 'Tab 1',
},
{
id: 'tab-2',
name: 'Tab 2',
},
{
id: 'tab-3',
name: 'Tab 3',
},
{
id: 'tab-4',
name: 'Tab 4',
},
{
id: 'tab-5',
name: 'Tab 5',
},
{
id: 'tab-6',
name: 'Tab 6',
},
{
id: 'tab-7',
name: 'Tab 7',
},
{
id: 'tab-8',
name: 'Tab 8',
},
{
id: 'tab-9',
name: 'Tab 9',
},
{
id: 'tab-10',
name: 'Tab 10',
},
]
interface EventBufferEntry {
event: NativeKeyEvent,
@@ -92,11 +26,13 @@ export class HotkeysService {
globalHotkey = new EventEmitter()
private currentKeystrokes: EventBufferEntry[] = []
private disabledLevel = 0
private hotkeyDescriptions: IHotkeyDescription[]
constructor(
private zone: NgZone,
private electron: ElectronService,
private config: ConfigService,
@Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[],
) {
let events = [
{
@@ -122,6 +58,7 @@ export class HotkeysService {
oldHandler.bind(this)(nativeEvent)
}
})
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
}
emitNativeEvent (name, nativeEvent) {
@@ -214,8 +151,8 @@ export class HotkeysService {
return result
}
getHotkeyDescription (id: string) : HotkeyDescription {
return HOTKEYS.filter((x) => x.id == id)[0]
getHotkeyDescription (id: string) : IHotkeyDescription {
return this.hotkeyDescriptions.filter((x) => x.id == id)[0]
}
enable () {
@@ -231,3 +168,70 @@ export class HotkeysService {
}
}
@Injectable()
export class AppHotkeyProvider extends HotkeyProvider {
hotkeys: IHotkeyDescription[] = [
{
id: 'new-tab',
name: 'New tab',
},
{
id: 'close-tab',
name: 'Close tab',
},
{
id: 'toggle-last-tab',
name: 'Toggle last tab',
},
{
id: 'next-tab',
name: 'Next tab',
},
{
id: 'previous-tab',
name: 'Previous tab',
},
{
id: 'tab-1',
name: 'Tab 1',
},
{
id: 'tab-2',
name: 'Tab 2',
},
{
id: 'tab-3',
name: 'Tab 3',
},
{
id: 'tab-4',
name: 'Tab 4',
},
{
id: 'tab-5',
name: 'Tab 5',
},
{
id: 'tab-6',
name: 'Tab 6',
},
{
id: 'tab-7',
name: 'Tab 7',
},
{
id: 'tab-8',
name: 'Tab 8',
},
{
id: 'tab-9',
name: 'Tab 9',
},
{
id: 'tab-10',
name: 'Tab 10',
},
]
}

View File

@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
export declare type ComponentType = new (...args: any[]) => Component
export abstract class SettingsProvider {
export abstract class SettingsTabProvider {
title: string
getComponentType (): ComponentType {

View File

@@ -0,0 +1,6 @@
.item(*ngFor='let item of model')
.body((click)='editItem(item)')
.stroke(*ngFor='let stroke of item') {{stroke}}
.remove((click)='removeItem(item)') &times;
.add((click)='addItem()') Add

View File

@@ -0,0 +1,28 @@
:host {
display: flex;
}
.item {
display: flex;
flex: none;
padding: 3px 0;
margin-right: 5px;
.body {
flex: none;
display: flex;
.stroke {
flex: none;
padding: 0 3px;
}
}
.remove {
flex: none;
}
}
.add {
flex: auto;
}

View File

@@ -0,0 +1,48 @@
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'
import { ModalService } from 'services/modal'
import { HotkeyInputModalComponent } from './hotkeyInputModal'
@Component({
selector: 'multi-hotkey-input',
template: require('./multiHotkeyInput.pug'),
styles: [require('./multiHotkeyInput.scss')],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MultiHotkeyInputComponent {
constructor (
private modal: ModalService,
) { }
ngOnInit () {
if (!this.model) {
this.model = []
}
if (typeof this.model == 'string') {
this.model = [this.model]
}
this.model = this.model.map(item => (typeof item == 'string') ? [item] : item)
}
editItem (item) {
this.modal.open(HotkeyInputModalComponent).result.then((value: string[]) => {
Object.assign(item, value)
this.modelChange.emit(this.model)
})
}
addItem () {
this.modal.open(HotkeyInputModalComponent).result.then((value: string[]) => {
this.model.push(value)
this.modelChange.emit(this.model)
})
}
removeItem (item) {
this.model = this.model.filter(x => x !== item)
this.modelChange.emit(this.model)
}
@Input() model: string[][]
@Output() modelChange = new EventEmitter()
}

View File

@@ -0,0 +1,10 @@
:host /deep/ ngb-tabset {
flex: auto;
}
:host /deep/ ngb-tabset > .tab-content {
padding: 15px 30px;
margin: 0;
flex: auto;
overflow-y: auto;
}

View File

@@ -1,6 +1,6 @@
button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
ngb-tabset(type='tabs')
ngb-tabset.vertical(type='tabs')
ngb-tab(*ngFor='let provider of settingsProviders')
template(ngbTabTitle)
| {{provider.title}}
@@ -134,10 +134,13 @@ ngb-tabset(type='tabs')
| Hotkeys
template(ngbTabContent)
.form-group
table.table
table
tr
th Toggle terminal window
td
hotkey-input('[(model)]'='globalHotkey')
hotkey-input('[(model)]'='globalHotkey')
tr(*ngFor='let hotkey of hotkeyDescriptions')
th {{hotkey.name}}
td
multi-hotkey-input('[(model)]'='config.store.hotkeys[hotkey.id]')

View File

@@ -1,6 +1,6 @@
:host {
display: flex;
flex: auto;
margin: 15px;
>.btn-block {
margin-bottom: 20px;

View File

@@ -2,27 +2,34 @@ import { Component, Inject } from '@angular/core'
import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking'
import { IHotkeyDescription, HotkeyProvider } from 'api/hotkeyProvider'
import { BaseTabComponent } from 'components/baseTab'
import { SettingsTab } from '../tab'
import { SettingsProvider } from '../api'
import { SettingsTabProvider } from '../api'
@Component({
selector: 'settings-pane',
template: require('./settingsPane.pug'),
styles: [require('./settingsPane.less')],
styles: [
require('./settingsPane.scss'),
require('./settingsPane.deep.css'),
],
})
export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> {
globalHotkey = ['Ctrl+Shift+G']
private hotkeyDescriptions: IHotkeyDescription[]
constructor(
public config: ConfigService,
private electron: ElectronService,
public docking: DockingService,
@Inject(SettingsProvider) public settingsProviders: SettingsProvider[]
@Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[],
@Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[]
) {
super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
}
ngOnDestroy () {

View File

@@ -1,12 +1,12 @@
import { Component, Input, ViewContainerRef, ViewChild, ComponentFactoryResolver, ComponentRef } from '@angular/core'
import { SettingsProvider } from '../api'
import { SettingsTabProvider } from '../api'
@Component({
selector: 'settings-tab-body',
template: '<template #placeholder></template>',
})
export class SettingsTabBodyComponent {
@Input() provider: SettingsProvider
@Input() provider: SettingsTabProvider
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
private component: ComponentRef<Component>

View File

@@ -7,6 +7,7 @@ import { HotkeyInputComponent } from './components/hotkeyInput'
import { HotkeyDisplayComponent } from './components/hotkeyDisplay'
import { HotkeyHintComponent } from './components/hotkeyHint'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput'
import { SettingsPaneComponent } from './components/settingsPane'
import { SettingsTabBodyComponent } from './components/settingsTabBody'
@@ -35,6 +36,7 @@ import { RecoveryProvider } from './recoveryProvider'
HotkeyHintComponent,
HotkeyInputComponent,
HotkeyInputModalComponent,
MultiHotkeyInputComponent,
SettingsPaneComponent,
SettingsTabBodyComponent,
],

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { SessionsService } from './services/sessions'
import { TerminalTab } from './tab'
@@ -9,8 +9,18 @@ export class ButtonProvider extends ToolbarButtonProvider {
constructor (
private app: AppService,
private sessions: SessionsService,
hotkeys: HotkeysService,
) {
super()
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey == 'new-tab') {
this.app.openTab(await this.getNewTab())
}
})
}
async getNewTab (): Promise<TerminalTab> {
return new TerminalTab(await this.sessions.createNewSession({ command: 'zsh' }))
}
provide (): IToolbarButton[] {
@@ -18,8 +28,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
icon: 'plus',
title: 'New terminal',
click: async () => {
let session = await this.sessions.createNewSession({ command: 'zsh' })
this.app.openTab(new TerminalTab(session))
this.app.openTab(await this.getNewTab())
}
}]
}

View File

@@ -3,8 +3,8 @@
.form-group
label Preview
.appearance-preview(
[style.font-family]='config.full().appearance.font',
[style.font-size]='config.full().appearance.fontSize + "px"',
[style.font-family]='config.full().terminal.font',
[style.font-size]='config.full().terminal.fontSize + "px"',
)
.text john@doe-pc$ ls
.text foo bar
@@ -14,7 +14,7 @@
input.form-control(
type='text',
[ngbTypeahead]='fontAutocomplete',
'[(ngModel)]'='config.store.appearance.font',
'[(ngModel)]'='config.store.terminal.font',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Font to be used in the terminal
@@ -23,7 +23,7 @@
label Font size
input.form-control(
type='number',
'[(ngModel)]'='config.store.appearance.fontSize',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Text size to be used in the terminal

View File

@@ -83,8 +83,8 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
configure () {
let config = this.config.full()
preferenceManager.set('font-family', config.appearance.font)
preferenceManager.set('font-size', config.appearance.fontSize)
preferenceManager.set('font-family', config.terminal.font)
preferenceManager.set('font-size', config.terminal.fontSize)
preferenceManager.set('audible-bell-sound', '')
preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification')
preferenceManager.set('enable-clipboard-notice', false)

View File

@@ -0,0 +1,24 @@
import { ConfigProvider } from 'api'
export class TerminalConfigProvider extends ConfigProvider {
defaultConfigValues: any = {
terminal: {
font: 'monospace',
fontSize: 14,
bell: 'off',
},
hotkeys: {
'new-tab': [
['Ctrl-A', 'C'],
['Ctrl-A', 'Ctrl-C'],
'Ctrl-Shift-T',
]
},
}
configStructure: any = {
terminal: {},
hotkeys: {},
}
}

View File

@@ -3,8 +3,8 @@ import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToolbarButtonProvider, TabRecoveryProvider } from 'api'
import { SettingsProvider } from '../settings/api'
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider } from 'api'
import { SettingsTabProvider } from '../settings/api'
import { TerminalTabComponent } from './components/terminalTab'
import { SettingsComponent } from './components/settings'
@@ -14,6 +14,7 @@ import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider'
import { SessionPersistenceProvider } from './api'
import { TerminalSettingsProvider } from './settings'
import { TerminalConfigProvider } from './config'
@NgModule({
imports: [
@@ -26,7 +27,8 @@ import { TerminalSettingsProvider } from './settings'
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
SessionsService,
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider },
{ provide: SettingsProvider, useClass: TerminalSettingsProvider, multi: true },
{ provide: SettingsTabProvider, useClass: TerminalSettingsProvider, multi: true },
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
],
entryComponents: [
TerminalTabComponent,

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core'
import { SettingsProvider, ComponentType } from '../settings/api'
import { SettingsTabProvider, ComponentType } from '../settings/api'
import { SettingsComponent } from './components/settings'
@Injectable()
export class TerminalSettingsProvider extends SettingsProvider {
export class TerminalSettingsProvider extends SettingsTabProvider {
title = 'Terminal'
getComponentType (): ComponentType {