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 { 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,

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')
terminal([session]='tab.session', '[(title)]'='tab.name')
hotkey-hint
toaster-container([toasterconfig]="toasterconfig")
template(ngbModalContainer)

View file

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

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;
&: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();
}

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({
selector: 'hotkey-input',
template: require('./hotkeyInput.pug'),
template: `
<hotkey-display [model]='model'></hotkey-display>
`,
styles: [require('./hotkeyInput.less')],
changeDetection: ChangeDetectionStrategy.OnPush,
})

View file

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

View file

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

View file

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

View file

@ -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() {

View file

@ -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<NativeKeyEvent>()
matchedHotkey = new EventEmitter<string>()
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]
}
}