mirror of
https://github.com/Eugeny/tabby
synced 2024-11-14 08:57:21 +00:00
.
This commit is contained in:
parent
13ec887d66
commit
2acc4f77d4
18 changed files with 284 additions and 128 deletions
|
@ -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,
|
||||
|
|
|
@ -189,3 +189,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
hotkey-hint {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'}))
|
||||
}
|
||||
|
|
24
app/src/components/hotkeyDisplay.less
Normal file
24
app/src/components/hotkeyDisplay.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
app/src/components/hotkeyDisplay.pug
Normal file
7
app/src/components/hotkeyDisplay.pug
Normal 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') +
|
37
app/src/components/hotkeyDisplay.ts
Normal file
37
app/src/components/hotkeyDisplay.ts
Normal 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[]
|
||||
}
|
8
app/src/components/hotkeyHint.less
Normal file
8
app/src/components/hotkeyHint.less
Normal file
|
@ -0,0 +1,8 @@
|
|||
:host {
|
||||
display: block;
|
||||
|
||||
.line {
|
||||
background: #333;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
4
app/src/components/hotkeyHint.pug
Normal file
4
app/src/components/hotkeyHint.pug
Normal 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 }}
|
59
app/src/components/hotkeyHint.ts
Normal file
59
app/src/components/hotkeyHint.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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') +
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue