This commit is contained in:
Eugene Pankov 2016-12-30 23:37:41 +03:00
parent 13ec887d66
commit 2acc4f77d4
18 changed files with 284 additions and 128 deletions

View File

@ -19,6 +19,8 @@ import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter'
import { AppComponent } from 'components/app' import { AppComponent } from 'components/app'
import { CheckboxComponent } from 'components/checkbox' import { CheckboxComponent } from 'components/checkbox'
import { HotkeyInputComponent } from 'components/hotkeyInput' import { HotkeyInputComponent } from 'components/hotkeyInput'
import { HotkeyDisplayComponent } from 'components/hotkeyDisplay'
import { HotkeyHintComponent } from 'components/hotkeyHint'
import { HotkeyInputModalComponent } from 'components/hotkeyInputModal' import { HotkeyInputModalComponent } from 'components/hotkeyInputModal'
import { SettingsModalComponent } from 'components/settingsModal' import { SettingsModalComponent } from 'components/settingsModal'
import { TerminalComponent } from 'components/terminal' import { TerminalComponent } from 'components/terminal'
@ -51,6 +53,8 @@ import { TerminalComponent } from 'components/terminal'
declarations: [ declarations: [
AppComponent, AppComponent,
CheckboxComponent, CheckboxComponent,
HotkeyDisplayComponent,
HotkeyHintComponent,
HotkeyInputComponent, HotkeyInputComponent,
HotkeyInputModalComponent, HotkeyInputModalComponent,
SettingsModalComponent, SettingsModalComponent,

View File

@ -189,3 +189,10 @@
} }
} }
} }
hotkey-hint {
position: absolute;
bottom: 0;
right: 0;
max-width: 300px;
}

View File

@ -29,6 +29,8 @@
.tab(*ngFor='let tab of tabs; trackBy: tab?.id', [class.active]='tab == activeTab') .tab(*ngFor='let tab of tabs; trackBy: tab?.id', [class.active]='tab == activeTab')
terminal([session]='tab.session', '[(title)]'='tab.name') terminal([session]='tab.session', '[(title)]'='tab.name')
hotkey-hint
toaster-container([toasterconfig]="toasterconfig") toaster-container([toasterconfig]="toasterconfig")
template(ngbModalContainer) template(ngbModalContainer)

View File

@ -49,6 +49,10 @@ class Tab {
] ]
}) })
export class AppComponent { export class AppComponent {
toasterConfig: ToasterConfig
tabs: Tab[] = []
activeTab: Tab
constructor( constructor(
private modal: ModalService, private modal: ModalService,
private elementRef: ElementRef, private elementRef: ElementRef,
@ -70,6 +74,17 @@ export class AppComponent {
timeout: 4000, 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) => { this.hotkeys.key.subscribe((key) => {
if (key.event == 'keydown') { if (key.event == 'keydown') {
if (key.alt && key.key >= '1' && key.key <= '9') { if (key.alt && key.key >= '1' && key.key <= '9') {
@ -83,12 +98,6 @@ export class AppComponent {
this.selectTab(this.tabs[9]) 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 () { newTab () {
this.addSessionTab(this.sessions.createNewSession({command: 'bash'})) this.addSessionTab(this.sessions.createNewSession({command: 'bash'}))
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
:host {
display: block;
.line {
background: #333;
padding: 3px 10px;
}
}

View File

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

View File

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

View File

@ -1,4 +1,7 @@
.button-states() { :host {
display: inline-block;
padding: 5px 10px;
transition: 0.125s all; transition: 0.125s all;
&:hover:not(.active) { &:hover:not(.active) {
@ -9,31 +12,3 @@
background: rgba(0, 0, 0, .1); 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();
}

View File

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

View File

@ -5,7 +5,9 @@ import { HotkeyInputModalComponent } from './hotkeyInputModal'
@Component({ @Component({
selector: 'hotkey-input', selector: 'hotkey-input',
template: require('./hotkeyInput.pug'), template: `
<hotkey-display [model]='model'></hotkey-display>
`,
styles: [require('./hotkeyInput.less')], styles: [require('./hotkeyInput.less')],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@ -3,42 +3,20 @@
padding: 30px 20px !important; 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 { .input {
background: #111; background: #111;
text-align: center;
font-size: 24px; font-size: 24px;
line-height: 24px; line-height: 24px;
height: 50px; height: 50px;
} }
.timeout { .timeout {
background: #333; background: #111;
height: 10px; height: 5px;
margin: 15px 0; margin: 0 0 15px;
div { div {
height: 10px; height: 5px;
background: #666; background: #666;
} }
} }

View File

@ -1,11 +1,8 @@
div.modal-body div.modal-body
label Press the key now label Press the key now
.input .input
.stroke(*ngFor='let stroke of value') hotkey-display([model]='value')
.key-container(*ngFor='let key of splitKeys(stroke); let isLast = last')
.key {{key}}
.plus(*ngIf='!isLast') +
.timeout .timeout
div([style.width]='timeoutProgress + "%"') div([style.width]='timeoutProgress + "%"')
a.btn.btn-default((click)='close()') Cancel a.btn.btn-default.pull-right((click)='close()') Cancel

View File

@ -23,6 +23,7 @@ export class HotkeyInputModalComponent {
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
public hotkeys: HotkeysService, public hotkeys: HotkeysService,
) { ) {
this.hotkeys.clearCurrentKeystrokes()
this.keySubscription = hotkeys.key.subscribe(() => { this.keySubscription = hotkeys.key.subscribe(() => {
this.lastKeyEvent = performance.now() this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes() this.value = this.hotkeys.getCurrentKeystrokes()

View File

@ -1,15 +1,10 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HostAppService, PLATFORM_MAC, PLATFORM_WINDOWS } from 'services/hostApp'
const Config = nodeRequire('electron-config') const Config = nodeRequire('electron-config')
const exec = nodeRequire('child-process-promise').exec
import * as fs from 'fs'
@Injectable() @Injectable()
export class ConfigService { export class ConfigService {
constructor( constructor() {
private hostApp: HostAppService,
) {
this.config = new Config({name: 'config'}) this.config = new Config({name: 'config'})
this.load() this.load()
} }
@ -17,65 +12,22 @@ export class ConfigService {
private config: any private config: any
private store: 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) { set(key: string, value: any) {
this.store.set(key, value)
this.save() this.save()
this.config.set(key, value)
this.load()
} }
get(key: string): any { get(key: string): any {
this.save() return this.store[key]
return this.config.get(key)
} }
has(key: string): boolean { has(key: string): boolean {
this.save() return this.store[key] != undefined
return this.config.has(key)
} }
delete(key: string) { delete(key: string) {
delete this.store[key]
this.save() this.save()
this.config.delete(key)
this.load()
} }
load() { load() {

View File

@ -1,9 +1,35 @@
import { Injectable, NgZone, EventEmitter } from '@angular/core' import { Injectable, NgZone, EventEmitter } from '@angular/core'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config'
import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util' import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util'
const hterm = require('hterm-commonjs') 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 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 { interface EventBufferEntry {
event: NativeKeyEvent, event: NativeKeyEvent,
@ -13,12 +39,14 @@ interface EventBufferEntry {
@Injectable() @Injectable()
export class HotkeysService { export class HotkeysService {
key = new EventEmitter<NativeKeyEvent>() key = new EventEmitter<NativeKeyEvent>()
matchedHotkey = new EventEmitter<string>()
globalHotkey = new EventEmitter() globalHotkey = new EventEmitter()
private currentKeystrokes: EventBufferEntry[] = [] private currentKeystrokes: EventBufferEntry[] = []
constructor( constructor(
private zone: NgZone, private zone: NgZone,
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService,
) { ) {
let events = [ let events = [
{ {
@ -42,6 +70,10 @@ export class HotkeysService {
oldHandler.bind(this)(nativeEvent) oldHandler.bind(this)(nativeEvent)
} }
}) })
if (!config.get('hotkeys')) {
config.set('hotkeys', {})
}
} }
emitNativeEvent (name, nativeEvent) { emitNativeEvent (name, nativeEvent) {
@ -51,10 +83,20 @@ export class HotkeysService {
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() }) this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
this.zone.run(() => { this.zone.run(() => {
let matched = this.getCurrentFullyMatchedHotkey()
if (matched) {
console.log('Matched hotkey', matched)
this.matchedHotkey.emit(matched)
this.clearCurrentKeystrokes()
}
this.key.emit(nativeEvent) this.key.emit(nativeEvent)
}) })
} }
clearCurrentKeystrokes () {
this.currentKeystrokes = []
}
getCurrentKeystrokes () : string[] { getCurrentKeystrokes () : string[] {
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT ) this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT )
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event)) return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event))
@ -66,4 +108,60 @@ export class HotkeysService {
this.globalHotkey.emit() 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]
}
} }