diff --git a/terminus-core/src/api/hotkeyProvider.ts b/terminus-core/src/api/hotkeyProvider.ts index fa3fe1b021c66626e93661f937b574bf3502ffc2..d1bb95b004a60935ac23afd530499e8c00c5b9f3 100644 --- a/terminus-core/src/api/hotkeyProvider.ts +++ b/terminus-core/src/api/hotkeyProvider.ts @@ -5,4 +5,6 @@ export interface IHotkeyDescription { export abstract class HotkeyProvider { hotkeys: IHotkeyDescription[] = [] + + abstract provide (): Promise } diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index e6433bd07c00d649cba6683f84fc854ce886d0f0..a57500abf3b7ee0048009a38a2dc18920e1b0c42 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -9,15 +9,19 @@ import { HostAppService } from './hostApp.service' const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s }) +function isStructuralMember (v) { + return v instanceof Object && !(v instanceof Array) && + Object.keys(v).length > 0 && !v.__nonStructural +} + +function isNonStructuralObjectMember (v) { + return v instanceof Object && !(v instanceof Array) && v.__nonStructural +} + export class ConfigProxy { constructor (real: any, defaults: any) { for (let key in defaults) { - if ( - defaults[key] instanceof Object && - !(defaults[key] instanceof Array) && - Object.keys(defaults[key]).length > 0 && - !defaults[key].__nonStructural - ) { + if (isStructuralMember(defaults[key])) { if (!real[key]) { real[key] = {} } @@ -38,15 +42,36 @@ export class ConfigProxy { { enumerable: true, configurable: false, - get: () => (real[key] !== undefined) ? real[key] : defaults[key], + get: () => this.getValue(key), set: (value) => { - real[key] = value + this.setValue(key, value) } } ) } } + + this.getValue = (key: string) => { + if (real[key] !== undefined) { + return real[key] + } else { + if (isNonStructuralObjectMember(defaults[key])) { + real[key] = {...defaults[key]} + delete real[key].__nonStructural + return real[key] + } else { + return defaults[key] + } + } + } + + this.setValue = (key: string, value: any) => { + real[key] = value + } } + + getValue (key: string): any { } // tslint:disable-line + setValue (key: string, value: any) { } // tslint:disable-line } @Injectable() diff --git a/terminus-core/src/services/hotkeys.service.ts b/terminus-core/src/services/hotkeys.service.ts index 760c0c73a45648e290573d303a56854ea431b4ab..de4da1cb3cdc122b257c0b49948892da1ce12015 100644 --- a/terminus-core/src/services/hotkeys.service.ts +++ b/terminus-core/src/services/hotkeys.service.ts @@ -24,13 +24,13 @@ export class HotkeysService { globalHotkey = new EventEmitter() private currentKeystrokes: EventBufferEntry[] = [] private disabledLevel = 0 - private hotkeyDescriptions: IHotkeyDescription[] + private hotkeyDescriptions: IHotkeyDescription[] = [] constructor ( private zone: NgZone, private electron: ElectronService, private config: ConfigService, - @Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[], + @Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[], ) { let events = ['keydown', 'keyup'] events.forEach((event) => { @@ -42,11 +42,13 @@ export class HotkeysService { } }) }) - this.hotkeyDescriptions = this.config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.config.changed$.subscribe(() => { this.registerGlobalHotkey() }) this.registerGlobalHotkey() + this.getHotkeyDescriptions().then(hotkeys => { + this.hotkeyDescriptions = hotkeys + }) } pushKeystroke (name, nativeEvent) { @@ -102,14 +104,25 @@ export class HotkeysService { } getHotkeysConfig () { + return this.getHotkeysConfigRecursive(this.config.store.hotkeys) + } + + getHotkeysConfigRecursive (branch) { let keys = {} - for (let key in this.config.store.hotkeys) { - let value = this.config.store.hotkeys[key] - if (typeof value === 'string') { - value = [value] + for (let key in branch) { + let value = branch[key] + if (value instanceof Object && !(value instanceof Array)) { + let subkeys = this.getHotkeysConfigRecursive(value) + for (let subkey in subkeys) { + keys[key + '.' + subkey] = subkeys[subkey] + } + } else { + if (typeof value === 'string') { + value = [value] + } + value = value.map((item) => (typeof item === 'string') ? [item] : item) + keys[key] = value } - value = value.map((item) => (typeof item === 'string') ? [item] : item) - keys[key] = value } return keys } @@ -169,6 +182,15 @@ export class HotkeysService { isEnabled () { return this.disabledLevel === 0 } + + async getHotkeyDescriptions (): Promise { + return ( + await Promise.all( + this.config.enabledServices(this.hotkeyProviders) + .map(async x => x.provide ? x.provide() : x.hotkeys) + ) + ).reduce((a, b) => a.concat(b)) + } } @Injectable() @@ -243,4 +265,8 @@ export class AppHotkeyProvider extends HotkeyProvider { name: 'Tab 10', }, ] + + async provide (): Promise { + return this.hotkeys + } } diff --git a/terminus-settings/src/components/settingsTab.component.pug b/terminus-settings/src/components/settingsTab.component.pug index ccb9e84d02dd76823ea05dca9c0914c0577b6032..6ddcd6600a1690eda11ee9370f6bfe2d01bec0cd 100644 --- a/terminus-settings/src/components/settingsTab.component.pug +++ b/terminus-settings/src/components/settingsTab.component.pug @@ -244,8 +244,8 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab') td {{hotkey.id}} td multi-hotkey-input( - [(model)]='config.store.hotkeys[hotkey.id]', - (modelChange)='config.save(); docking.dock()' + [model]='getHotkey(hotkey.id)', + (modelChange)='setHotkey(hotkey.id, $event); config.save(); docking.dock()' ) ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id') diff --git a/terminus-settings/src/components/settingsTab.component.ts b/terminus-settings/src/components/settingsTab.component.ts index 6c531f9dab577faa596f482e6be29478b569698b..238cbe952a8cb007eecf2f6f4ee8d080c4ff3912 100644 --- a/terminus-settings/src/components/settingsTab.component.ts +++ b/terminus-settings/src/components/settingsTab.component.ts @@ -1,6 +1,7 @@ import * as yaml from 'js-yaml' import { Subscription } from 'rxjs' import { Component, Inject, Input } from '@angular/core' +import { HotkeysService } from 'terminus-core' import { ElectronService, DockingService, @@ -43,12 +44,12 @@ export class SettingsTabComponent extends BaseTabComponent { public hostApp: HostAppService, public homeBase: HomeBaseService, public shellIntegration: ShellIntegrationService, + hotkeys: HotkeysService, @Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[], @Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[], @Inject(Theme) public themes: Theme[], ) { super() - this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.setTitle('Settings') this.screens = this.docking.getScreens() this.settingsProviders = config.enabledServices(this.settingsProviders) @@ -59,6 +60,10 @@ export class SettingsTabComponent extends BaseTabComponent { this.configSubscription = config.changed$.subscribe(() => { this.configFile = config.readRaw() }) + + hotkeys.getHotkeyDescriptions().then(descriptions => { + this.hotkeyDescriptions = descriptions + }) } async ngOnInit () { @@ -98,4 +103,22 @@ export class SettingsTabComponent extends BaseTabComponent { await this.shellIntegration.install() this.isShellIntegrationInstalled = true } + + getHotkey (id: string) { + let ptr = this.config.store.hotkeys + for (let token of id.split(/\./g)) { + ptr = ptr[token] + } + return ptr + } + + setHotkey (id: string, value) { + let ptr = this.config.store + let prop = 'hotkeys' + for (let token of id.split(/\./g)) { + ptr = ptr[prop] + prop = token + } + ptr[prop] = value + } } diff --git a/terminus-terminal/src/config.ts b/terminus-terminal/src/config.ts index a378b7d70fce51ebf84dade5d88895b602932383..1c4a9bbeefd650aa995fab227dadad79b6dbd48c 100644 --- a/terminus-terminal/src/config.ts +++ b/terminus-terminal/src/config.ts @@ -2,6 +2,11 @@ import { ConfigProvider, Platform } from 'terminus-core' export class TerminalConfigProvider extends ConfigProvider { defaults = { + hotkeys: { + shell: { + __nonStructural: true, + }, + }, terminal: { frontend: 'hterm', autoOpen: false, diff --git a/terminus-terminal/src/hotkeys.ts b/terminus-terminal/src/hotkeys.ts index c73dcb3853d4931181ad778ab23dd8a00e2fe5e2..409a8f261f9f89753b48fba34855e0211bbdf100 100644 --- a/terminus-terminal/src/hotkeys.ts +++ b/terminus-terminal/src/hotkeys.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core' import { IHotkeyDescription, HotkeyProvider } from 'terminus-core' +import { TerminalService } from './services/terminal.service' @Injectable() export class TerminalHotkeyProvider extends HotkeyProvider { @@ -61,4 +62,16 @@ export class TerminalHotkeyProvider extends HotkeyProvider { name: 'Intelligent Ctrl-C (copy/abort)', }, ] + + constructor ( + private terminal: TerminalService, + ) { super() } + + async provide (): Promise { + let shells = await this.terminal.shells$.toPromise() + return this.hotkeys.concat(shells.map(shell => ({ + id: `shell.${shell.id}`, + name: `New tab: ${shell.name}` + }))) + } } diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index 34979e77f4e5643154252bf254d18df1447f68ae..01a4c504da82c016f3a0682889da2c9d578714ab 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -148,11 +148,16 @@ export default class TerminalModule { if (hotkey === 'new-tab') { terminal.openTab() } - }) - hotkeys.matchedHotkey.subscribe(async (hotkey) => { if (hotkey === 'new-window') { hostApp.newWindow() } + if (hotkey.startsWith('shell.')) { + let shells = await terminal.shells$ + let shell = shells.find(x => x.id === hotkey.split('.')[1]) + if (shell) { + terminal.openTab(shell) + } + } }) hostApp.cliOpenDirectory$.subscribe(async directory => { if (await fs.exists(directory)) { diff --git a/terminus-terminal/src/services/terminal.service.ts b/terminus-terminal/src/services/terminal.service.ts index 4e21107ce2b889ec472bdddab9b85ad92048b0c8..56e8b65035474601b77e2c94a8c880c8e52bc209 100644 --- a/terminus-terminal/src/services/terminal.service.ts +++ b/terminus-terminal/src/services/terminal.service.ts @@ -27,10 +27,14 @@ export class TerminalService { }) } + async getShells (): Promise { + let shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide())) + return shellLists.reduce((a, b) => a.concat(b)) + } + async reloadShells () { this.shells = new AsyncSubject() - 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(await this.getShells()) this.shells.complete() } diff --git a/terminus-terminal/src/shells/custom.ts b/terminus-terminal/src/shells/custom.ts index 7a6fa0b1f336f24b798a67274e3132a62c9e2ab3..223f1881cf2cc7a77abf126457bafbf97c55ee52 100644 --- a/terminus-terminal/src/shells/custom.ts +++ b/terminus-terminal/src/shells/custom.ts @@ -15,7 +15,7 @@ export class CustomShellProvider extends ShellProvider { let args = this.config.store.terminal.customShell.split(' ') return [{ id: 'custom', - name: 'Custom', + name: 'Custom shell', command: args[0], args: args.slice(1), }]