diff --git a/app/src/app.module.ts b/app/src/app.module.ts
index 68cb51f0..de921018 100644
--- a/app/src/app.module.ts
+++ b/app/src/app.module.ts
@@ -19,6 +19,8 @@ import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter'
import { AppComponent } from 'components/app'
import { CheckboxComponent } from 'components/checkbox'
import { HotkeyInputComponent } from 'components/hotkeyInput'
+import { HotkeyDisplayComponent } from 'components/hotkeyDisplay'
+import { HotkeyHintComponent } from 'components/hotkeyHint'
import { HotkeyInputModalComponent } from 'components/hotkeyInputModal'
import { SettingsModalComponent } from 'components/settingsModal'
import { TerminalComponent } from 'components/terminal'
@@ -51,6 +53,8 @@ import { TerminalComponent } from 'components/terminal'
declarations: [
AppComponent,
CheckboxComponent,
+ HotkeyDisplayComponent,
+ HotkeyHintComponent,
HotkeyInputComponent,
HotkeyInputModalComponent,
SettingsModalComponent,
diff --git a/app/src/components/app.less b/app/src/components/app.less
index 015a70da..fefacf17 100644
--- a/app/src/components/app.less
+++ b/app/src/components/app.less
@@ -189,3 +189,10 @@
}
}
}
+
+hotkey-hint {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ max-width: 300px;
+}
diff --git a/app/src/components/app.pug b/app/src/components/app.pug
index 978c6b76..0f3735e2 100644
--- a/app/src/components/app.pug
+++ b/app/src/components/app.pug
@@ -29,6 +29,8 @@
.tab(*ngFor='let tab of tabs; trackBy: tab?.id', [class.active]='tab == activeTab')
terminal([session]='tab.session', '[(title)]'='tab.name')
+hotkey-hint
+
toaster-container([toasterconfig]="toasterconfig")
template(ngbModalContainer)
diff --git a/app/src/components/app.ts b/app/src/components/app.ts
index dd1b4290..8022c3f3 100644
--- a/app/src/components/app.ts
+++ b/app/src/components/app.ts
@@ -49,6 +49,10 @@ class Tab {
]
})
export class AppComponent {
+ toasterConfig: ToasterConfig
+ tabs: Tab[] = []
+ activeTab: Tab
+
constructor(
private modal: ModalService,
private elementRef: ElementRef,
@@ -70,6 +74,17 @@ export class AppComponent {
timeout: 4000,
})
+ this.hotkeys.matchedHotkey.subscribe((hotkey) => {
+ if (hotkey == 'new-tab') {
+ this.newTab()
+ }
+ if (hotkey == 'close-tab') {
+ if (this.activeTab) {
+ this.closeTab(this.activeTab)
+ }
+ }
+ })
+
this.hotkeys.key.subscribe((key) => {
if (key.event == 'keydown') {
if (key.alt && key.key >= '1' && key.key <= '9') {
@@ -83,12 +98,6 @@ export class AppComponent {
this.selectTab(this.tabs[9])
}
}
- if (key.ctrl && key.shift && key.key == 'W' && this.activeTab) {
- this.closeTab(this.activeTab)
- }
- if (key.ctrl && key.shift && key.key == 'T' && this.activeTab) {
- this.newTab()
- }
}
})
@@ -98,10 +107,6 @@ export class AppComponent {
})
}
- toasterConfig: ToasterConfig
- tabs: Tab[] = []
- activeTab: Tab
-
newTab () {
this.addSessionTab(this.sessions.createNewSession({command: 'bash'}))
}
diff --git a/app/src/components/hotkeyDisplay.less b/app/src/components/hotkeyDisplay.less
new file mode 100644
index 00000000..ff8563ac
--- /dev/null
+++ b/app/src/components/hotkeyDisplay.less
@@ -0,0 +1,24 @@
+:host {
+ display: inline-block;
+
+ .stroke {
+ display: inline-block;
+ margin: 0 5px;
+
+ .key-container {
+ display: inline-block;
+ background: #222;
+ text-shadow: 0 1px 0 rgba(0,0,0,.5);
+
+ .key {
+ display: inline-block;
+ padding: 4px 5px;
+ }
+
+ .plus {
+ display: inline-block;
+ padding: 4px 2px;
+ }
+ }
+ }
+}
diff --git a/app/src/components/hotkeyDisplay.pug b/app/src/components/hotkeyDisplay.pug
new file mode 100644
index 00000000..ac511e99
--- /dev/null
+++ b/app/src/components/hotkeyDisplay.pug
@@ -0,0 +1,7 @@
+.stroke(*ngFor='let stroke of model')
+ .key-container(
+ *ngFor='let key of splitKeys(stroke); let isLast = last; trackBy: key',
+ @animateKey
+ )
+ .key {{key}}
+ .plus(*ngIf='!isLast') +
diff --git a/app/src/components/hotkeyDisplay.ts b/app/src/components/hotkeyDisplay.ts
new file mode 100644
index 00000000..aadc6c4e
--- /dev/null
+++ b/app/src/components/hotkeyDisplay.ts
@@ -0,0 +1,37 @@
+import { Component, Input, ChangeDetectionStrategy, trigger, style, animate, transition, state } from '@angular/core'
+
+
+@Component({
+ selector: 'hotkey-display',
+ template: require('./hotkeyDisplay.pug'),
+ styles: [require('./hotkeyDisplay.less')],
+ //changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ trigger('animateKey', [
+ state('in', style({
+ 'transform': 'translateX(0)',
+ 'opacity': '1',
+ })),
+ transition(':enter', [
+ style({
+ 'transform': 'translateX(25px)',
+ 'opacity': '0',
+ }),
+ animate('250ms ease-out')
+ ]),
+ transition(':leave', [
+ animate('250ms ease-in', style({
+ 'transform': 'translateX(25px)',
+ 'opacity': '0',
+ }))
+ ])
+ ])
+ ]
+})
+export class HotkeyDisplayComponent {
+ splitKeys(keys: string): string[] {
+ return keys.split('+').map((x) => x.trim())
+ }
+
+ @Input() model: string[]
+}
diff --git a/app/src/components/hotkeyHint.less b/app/src/components/hotkeyHint.less
new file mode 100644
index 00000000..67bdc457
--- /dev/null
+++ b/app/src/components/hotkeyHint.less
@@ -0,0 +1,8 @@
+:host {
+ display: block;
+
+ .line {
+ background: #333;
+ padding: 3px 10px;
+ }
+}
diff --git a/app/src/components/hotkeyHint.pug b/app/src/components/hotkeyHint.pug
new file mode 100644
index 00000000..a7c8908d
--- /dev/null
+++ b/app/src/components/hotkeyHint.pug
@@ -0,0 +1,4 @@
+.body(*ngIf='partialHotkeyMatches?.length > 0')
+ .line(*ngFor='let match of partialHotkeyMatches; trackBy: match?.id', @animateLine)
+ hotkey-display([model]='match.strokes')
+ span {{ hotkeys.getHotkeyDescription(match.id).name }}
diff --git a/app/src/components/hotkeyHint.ts b/app/src/components/hotkeyHint.ts
new file mode 100644
index 00000000..3e1797f0
--- /dev/null
+++ b/app/src/components/hotkeyHint.ts
@@ -0,0 +1,59 @@
+import { Component, ChangeDetectionStrategy, trigger, style, animate, transition, state } from '@angular/core'
+import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys'
+
+
+@Component({
+ selector: 'hotkey-hint',
+ template: require('./hotkeyHint.pug'),
+ styles: [require('./hotkeyHint.less')],
+ //changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ trigger('animateLine', [
+ state('in', style({
+ 'transform': 'translateX(0)',
+ 'opacity': '1',
+ })),
+ transition(':enter', [
+ style({
+ 'transform': 'translateX(25px)',
+ 'opacity': '0',
+ }),
+ animate('250ms ease-out')
+ ]),
+ transition(':leave', [
+ style({'height': '*'}),
+ animate('250ms ease-in', style({
+ 'transform': 'translateX(25px)',
+ 'opacity': '0',
+ 'height': '0',
+ }))
+ ])
+ ])
+ ]
+})
+export class HotkeyHintComponent {
+ partialHotkeyMatches: PartialHotkeyMatch[]
+ private keyTimeoutInterval: NodeJS.Timer = null
+
+ constructor (
+ public hotkeys: HotkeysService,
+ ) {
+ this.hotkeys.key.subscribe(() => {
+ let partialMatches = this.hotkeys.getCurrentPartiallyMatchedHotkeys()
+ if (partialMatches.length > 0) {
+ console.log('Partial matches:', partialMatches)
+ this.partialHotkeyMatches = partialMatches
+
+ if (this.keyTimeoutInterval == null) {
+ this.keyTimeoutInterval = setInterval(() => {
+ if (this.hotkeys.getCurrentPartiallyMatchedHotkeys().length == 0) {
+ clearInterval(this.keyTimeoutInterval)
+ this.keyTimeoutInterval = null
+ this.partialHotkeyMatches = null
+ }
+ }, 500)
+ }
+ }
+ })
+ }
+}
diff --git a/app/src/components/hotkeyInput.less b/app/src/components/hotkeyInput.less
index ce5177d1..5009adde 100644
--- a/app/src/components/hotkeyInput.less
+++ b/app/src/components/hotkeyInput.less
@@ -1,4 +1,7 @@
-.button-states() {
+:host {
+ display: inline-block;
+ padding: 5px 10px;
+
transition: 0.125s all;
&:hover:not(.active) {
@@ -9,31 +12,3 @@
background: rgba(0, 0, 0, .1);
}
}
-
-:host {
- display: inline-block;
- padding: 5px 10px;
-
- .stroke {
- display: inline-block;
- margin-right: 5px;
-
- .key-container {
- display: inline-block;
-
- .key {
- display: inline-block;
- padding: 4px 5px;
- background: #333;
- text-shadow: 0 1px 0 rgba(0,0,0,.5);
- }
-
- .plus {
- display: inline-block;
- margin: 0 5px;
- }
- }
- }
-
- .button-states();
-}
diff --git a/app/src/components/hotkeyInput.pug b/app/src/components/hotkeyInput.pug
deleted file mode 100644
index b0549382..00000000
--- a/app/src/components/hotkeyInput.pug
+++ /dev/null
@@ -1,4 +0,0 @@
-.stroke(*ngFor='let stroke of model')
- .key-container(*ngFor='let key of splitKeys(stroke); let isLast = last')
- .key {{key}}
- .plus(*ngIf='!isLast') +
diff --git a/app/src/components/hotkeyInput.ts b/app/src/components/hotkeyInput.ts
index 1953d0de..6c366b9f 100644
--- a/app/src/components/hotkeyInput.ts
+++ b/app/src/components/hotkeyInput.ts
@@ -5,7 +5,9 @@ import { HotkeyInputModalComponent } from './hotkeyInputModal'
@Component({
selector: 'hotkey-input',
- template: require('./hotkeyInput.pug'),
+ template: `
+
+ `,
styles: [require('./hotkeyInput.less')],
changeDetection: ChangeDetectionStrategy.OnPush,
})
diff --git a/app/src/components/hotkeyInputModal.less b/app/src/components/hotkeyInputModal.less
index 833d869c..85c3710b 100644
--- a/app/src/components/hotkeyInputModal.less
+++ b/app/src/components/hotkeyInputModal.less
@@ -3,42 +3,20 @@
padding: 30px 20px !important;
}
- .stroke {
- display: inline-block;
- margin: 8px 5px 0 0;
-
- .key-container {
- display: inline-block;
-
- .key {
- display: inline-block;
- padding: 4px 5px;
- background: #333;
- text-shadow: 0 1px 0 rgba(0,0,0,.5);
- }
-
- .plus {
- display: inline-block;
- margin: 0 5px;
- }
- }
- }
-
.input {
background: #111;
- text-align: center;
font-size: 24px;
line-height: 24px;
height: 50px;
}
.timeout {
- background: #333;
- height: 10px;
- margin: 15px 0;
+ background: #111;
+ height: 5px;
+ margin: 0 0 15px;
div {
- height: 10px;
+ height: 5px;
background: #666;
}
}
diff --git a/app/src/components/hotkeyInputModal.pug b/app/src/components/hotkeyInputModal.pug
index bb8411d4..88c0ef1a 100644
--- a/app/src/components/hotkeyInputModal.pug
+++ b/app/src/components/hotkeyInputModal.pug
@@ -1,11 +1,8 @@
div.modal-body
label Press the key now
.input
- .stroke(*ngFor='let stroke of value')
- .key-container(*ngFor='let key of splitKeys(stroke); let isLast = last')
- .key {{key}}
- .plus(*ngIf='!isLast') +
+ hotkey-display([model]='value')
.timeout
div([style.width]='timeoutProgress + "%"')
- a.btn.btn-default((click)='close()') Cancel
+ a.btn.btn-default.pull-right((click)='close()') Cancel
diff --git a/app/src/components/hotkeyInputModal.ts b/app/src/components/hotkeyInputModal.ts
index 7cc3b8f2..21e75081 100644
--- a/app/src/components/hotkeyInputModal.ts
+++ b/app/src/components/hotkeyInputModal.ts
@@ -23,6 +23,7 @@ export class HotkeyInputModalComponent {
private modalInstance: NgbActiveModal,
public hotkeys: HotkeysService,
) {
+ this.hotkeys.clearCurrentKeystrokes()
this.keySubscription = hotkeys.key.subscribe(() => {
this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes()
diff --git a/app/src/services/config.ts b/app/src/services/config.ts
index b5a602f6..64e0fdaf 100644
--- a/app/src/services/config.ts
+++ b/app/src/services/config.ts
@@ -1,15 +1,10 @@
import { Injectable } from '@angular/core'
-import { HostAppService, PLATFORM_MAC, PLATFORM_WINDOWS } from 'services/hostApp'
const Config = nodeRequire('electron-config')
-const exec = nodeRequire('child-process-promise').exec
-import * as fs from 'fs'
@Injectable()
export class ConfigService {
- constructor(
- private hostApp: HostAppService,
- ) {
+ constructor() {
this.config = new Config({name: 'config'})
this.load()
}
@@ -17,65 +12,22 @@ export class ConfigService {
private config: any
private store: any
- migrate() {
- if (!this.has('migrated')) {
- if (this.hostApp.platform == PLATFORM_WINDOWS) {
- let configPath = `${this.hostApp.getPath('documents')}\\.elements.conf`
- let config = null
- try {
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
- console.log('Migrating configuration:', config)
- this.set('host', config.Hostname)
- this.set('username', config.Username)
- this.set('firstDrive', config.FirstDrive)
- } catch (err) {
- console.error('Could not migrate the config:', err)
- }
- this.set('migrated', 1)
- this.save()
- return Promise.resolve()
- }
- if (this.hostApp.platform == PLATFORM_MAC) {
- return Promise.all([
- exec('defaults read ~/Library/Preferences/com.syslink.Elements.plist connection_host').then((result) => {
- this.set('host', result.stdout.trim())
- }),
- exec('defaults read ~/Library/Preferences/com.syslink.Elements.plist connection_username').then((result) => {
- this.set('username', result.stdout.trim())
- }),
- ]).then(() => {
- this.set('migrated', 1)
- this.save()
- }).catch((err) => {
- console.error('Could not migrate the config:', err)
- this.set('migrated', 1)
- this.save()
- })
- }
- }
- return Promise.resolve()
- }
-
set(key: string, value: any) {
+ this.store.set(key, value)
this.save()
- this.config.set(key, value)
- this.load()
}
get(key: string): any {
- this.save()
- return this.config.get(key)
+ return this.store[key]
}
has(key: string): boolean {
- this.save()
- return this.config.has(key)
+ return this.store[key] != undefined
}
delete(key: string) {
+ delete this.store[key]
this.save()
- this.config.delete(key)
- this.load()
}
load() {
diff --git a/app/src/services/hotkeys.ts b/app/src/services/hotkeys.ts
index 48038172..7034c46d 100644
--- a/app/src/services/hotkeys.ts
+++ b/app/src/services/hotkeys.ts
@@ -1,9 +1,35 @@
import { Injectable, NgZone, EventEmitter } from '@angular/core'
import { ElectronService } from 'services/electron'
+import { ConfigService } from 'services/config'
import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util'
const hterm = require('hterm-commonjs')
+export interface HotkeyDescription {
+ id: string,
+ name: string,
+ defaults: string[][],
+}
+
+export interface PartialHotkeyMatch {
+ id: string,
+ strokes: string[],
+ matchedLength: number,
+}
+
const KEY_TIMEOUT = 2000
+const HOTKEYS: HotkeyDescription[] = [
+ {
+ id: 'new-tab',
+ name: 'New tab',
+ defaults: [['Ctrl+Shift+T'], ['Ctrl+A', 'C']],
+ },
+ {
+ id: 'close-tab',
+ name: 'Close tab',
+ defaults: [['Ctrl+Shift+W'], ['Ctrl+A', 'K']],
+ },
+]
+
interface EventBufferEntry {
event: NativeKeyEvent,
@@ -13,12 +39,14 @@ interface EventBufferEntry {
@Injectable()
export class HotkeysService {
key = new EventEmitter()
+ matchedHotkey = new EventEmitter()
globalHotkey = new EventEmitter()
private currentKeystrokes: EventBufferEntry[] = []
constructor(
private zone: NgZone,
private electron: ElectronService,
+ private config: ConfigService,
) {
let events = [
{
@@ -42,6 +70,10 @@ export class HotkeysService {
oldHandler.bind(this)(nativeEvent)
}
})
+
+ if (!config.get('hotkeys')) {
+ config.set('hotkeys', {})
+ }
}
emitNativeEvent (name, nativeEvent) {
@@ -51,10 +83,20 @@ export class HotkeysService {
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
this.zone.run(() => {
+ let matched = this.getCurrentFullyMatchedHotkey()
+ if (matched) {
+ console.log('Matched hotkey', matched)
+ this.matchedHotkey.emit(matched)
+ this.clearCurrentKeystrokes()
+ }
this.key.emit(nativeEvent)
})
}
+ clearCurrentKeystrokes () {
+ this.currentKeystrokes = []
+ }
+
getCurrentKeystrokes () : string[] {
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT )
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event))
@@ -66,4 +108,60 @@ export class HotkeysService {
this.globalHotkey.emit()
})
}
+
+ getHotkeysConfig () {
+ let keys = {}
+ for (let key of HOTKEYS) {
+ keys[key.id] = key.defaults
+ }
+ for (let key in this.config.get('hotkeys')) {
+ keys[key] = this.config.get('hotkeys')[key]
+ }
+ return keys
+ }
+
+ getCurrentFullyMatchedHotkey () : string {
+ for (let id in this.getHotkeysConfig()) {
+ for (let sequence of this.getHotkeysConfig()[id]) {
+ let currentStrokes = this.getCurrentKeystrokes()
+ if (currentStrokes.length < sequence.length) {
+ break
+ }
+ if (sequence.every((x, index) => {
+ return x.toLowerCase() == currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase()
+ })) {
+ return id
+ }
+ }
+ }
+ return null
+ }
+
+ getCurrentPartiallyMatchedHotkeys () : PartialHotkeyMatch[] {
+ let result = []
+ for (let id in this.getHotkeysConfig()) {
+ for (let sequence of this.getHotkeysConfig()[id]) {
+ let currentStrokes = this.getCurrentKeystrokes()
+
+ for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) {
+ console.log(sequence, currentStrokes.slice(currentStrokes.length - sequence.length))
+ if (sequence.slice(0, matchLength).every((x, index) => {
+ return x.toLowerCase() == currentStrokes[currentStrokes.length - matchLength + index].toLowerCase()
+ })) {
+ result.push({
+ matchedLength: matchLength,
+ id,
+ strokes: sequence
+ })
+ break
+ }
+ }
+ }
+ }
+ return result
+ }
+
+ getHotkeyDescription (id: string) : HotkeyDescription {
+ return HOTKEYS.filter((x) => x.id == id)[0]
+ }
}