This commit is contained in:
Eugene Pankov 2017-03-05 17:20:47 +01:00
parent b7745bdd5b
commit 8beda026c1
27 changed files with 587 additions and 245 deletions

View File

@ -6,7 +6,7 @@ SHORT_VERSION=$(shell python -c 'import subprocess; v = subprocess.check_output(
all: run all: run
run: run:
DEV=1 ./node_modules/.bin/electron ./app DEV=1 ./node_modules/.bin/electron ./app --debug
lint: lint:
tslint -c tslint.json app/src/*.ts app/src/**/*.ts tslint -c tslint.json app/src/*.ts app/src/**/*.ts

View File

@ -1,141 +0,0 @@
@brand-primary: #f7e61d;
@brand-success: #5cb85c;
@brand-info: #5bc0de;
@brand-warning: #f0ad4e;
@brand-danger: #FF1C01;
// New
@brand-primary: #f7e61d;
@brand-success: #42B500;
@brand-info: #01BAEF;
@brand-warning: #DB8A00;
@brand-danger: #EF2F00;
@control-shadow: 0 1px 1px rgba(0,0,0,.25);
@control-shadow-active: 0 1px 1px rgba(0,0,0,.25) inset, @control-shadow;
@control-dropdown-shadow: 0 0 50px rgba(0,0,0,.5), @control-shadow;
@form-accent: #DBCA00;
@form-accent-bright: @brand-primary;
@body-bg: #1D272D;
@text-color: #aaa;
@font-family-sans-serif: "Source Sans Pro", "PT Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
@icon-font-path: "../fonts/";
@component-active-color: rgba(0,0,0,.15);
@component-active-color: darken(@component-active-bg, 30%);
@table-bg: #444;
@table-bg-accent: rgba(255,255,255,.15);
@table-bg-hover: #666;
@table-border-color: #2e2e2e;
@table-line-border-color: #4f4f4f;
@btn-default-color: @text-color;
@btn-default-bg: #243D49;
@btn-default-border: transparent;
@btn-primary-color: @component-active-color;
@btn-primary-border: #584E00;
@btn-danger-border: rgba(0,0,0,.5);
@btn-danger-color: white;
@btn-danger-bg: #FF4630;
@btn-danger-border: transparent;//@brand-danger;
@btn-link-disabled-color: darken(@text-color, 20%);
@input-bg: #11181C;
@input-bg-disabled: #2a2f31;
@input-color: #bbb;
@input-border: #3a3a3a;
@input-group-addon-border-color: @input-bg;
@input-color-placeholder: #777;
@input-group-addon-bg: @input-bg;
@dropdown-bg: rgba(64,64,64,.95); //@body-bg;
@dropdown-link-color: @text-color;
@dropdown-link-hover-color: #ddd;
@dropdown-link-hover-bg: #444;
@dropdown-link-disabled-color: darken(@text-color, 5%);
@navbar-default-bg: #23272A;
@navbar-default-border: #111;
@nav-tabs-border-color: #666;
@nav-tabs-link-hover-border-color: transparent;
@nav-tabs-active-link-hover-bg: rgba(255,255,255,.1);
@nav-tabs-active-link-hover-color: @brand-primary;
@nav-tabs-active-link-border-color: @brand-primary;
@nav-tabs-active-link-hover-border-color: @brand-primary;
@pagination-color: @btn-default-color;
@pagination-bg: @btn-default-bg;
@pagination-border: @btn-default-border;
@pagination-hover-color: @btn-default-color;
@pagination-hover-bg: lighten(@btn-default-bg, 5%);
@pagination-hover-border: @btn-default-border;
@pagination-active-color: @brand-primary;
@pagination-active-bg: darken(@btn-default-bg, 5%);
@pagination-active-border: @btn-default-border;
@pagination-disabled-color: @btn-link-disabled-color;
@pagination-disabled-bg: darken(@btn-default-bg, 5%);
@pagination-disabled-border: @btn-default-border;
//@state-success-bg: #234116; //#dff0d8;
//@state-info-bg: #0C3A50; //#d9edf7;
//@state-danger-bg: #9E3B3B;///#f2dede;
@popover-bg: rgba(64,64,64,.95);
@popover-arrow-color: #444;
@progress-bg: #555;
@list-group-bg: rgba(255,255,255,.1);
@list-group-disabled-bg: #333;
@list-group-border: transparent;
@list-group-line-border: rgba(255,255,255,.1);
@list-group-hover-bg: rgba(255,255,255,.2);
@list-group-link-color: @text-color;
@list-group-link-heading-color: @text-color;
@list-group-active-bg: rgba(255,255,255,.3);
@list-group-active-border: transparent;
@panel-bg: @table-bg;
@panel-inner-border: @table-border-color;
@panel-footer-bg: #666;
@panel-default-text: #ddd;
@panel-default-border: @table-border-color;
@panel-default-heading-bg: #666;
@well-bg: #222;
@badge-bg: #333;
@badge-active-bg: #333;
@code-bg: #222;
@pre-bg: #222;
@pre-color: #bbb;
@blockquote-border-color: #444;
@page-header-border-color: #444;
@alert-bg: #2A2A2A;
@state-success-text: @brand-success;
@state-success-bg: @alert-bg;
@state-success-border: @state-success-text;
@state-info-text: @brand-info;
@state-info-bg: @alert-bg;
@state-info-border: @state-info-text;
@state-warning-text: #F27208;
@state-warning-bg: @alert-bg;
@state-warning-border: @state-warning-text;
@state-danger-text: @brand-danger;
@state-danger-bg: @alert-bg;
@state-danger-border: @state-danger-text;
@border-radius-base: 1px;
@border-radius-large: 3px;
@border-radius-small: 1px;

47
app/defaultConfig.yaml Normal file
View File

@ -0,0 +1,47 @@
hotkeys:
new-tab:
- ['Ctrl-A', 'C']
- ['Ctrl-A', 'Ctrl-C']
- 'Ctrl-Shift-T'
close-tab:
- 'Ctrl-Shift-W'
- ['Ctrl-A', 'K']
toggle-last-tab:
- ['Ctrl-A', 'A']
- ['Ctrl-A', 'Ctrl-A']
next-tab:
- 'Ctrl-Shift-ArrowRight'
- ['Ctrl-A', 'N']
previous-tab:
- 'Ctrl-Shift-ArrowLeft'
- ['Ctrl-A', 'P']
tab-1:
- 'Alt-1'
- ['Ctrl-A', '1']
tab-2:
- 'Alt-2'
- ['Ctrl-A', '2']
tab-3:
- 'Alt-3'
- ['Ctrl-A', '3']
tab-4:
- 'Alt-4'
- ['Ctrl-A', '4']
tab-5:
- 'Alt-5'
- ['Ctrl-A', '5']
tab-6:
- 'Alt-6'
- ['Ctrl-A', '6']
tab-7:
- 'Alt-7'
- ['Ctrl-A', '7']
tab-8:
- 'Alt-8'
- ['Ctrl-A', '8']
tab-9:
- 'Alt-9'
- ['Ctrl-A', '9']
tab-10:
- 'Alt-0'
- ['Ctrl-A', '0']

View File

@ -10,5 +10,8 @@
"electron-is-dev": "^0.1.2", "electron-is-dev": "^0.1.2",
"path": "^0.12.7", "path": "^0.12.7",
"pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642" "pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642"
},
"devDependencies": {
"js-yaml": "^3.8.2"
} }
} }

View File

@ -12,6 +12,7 @@ import { LogService } from 'services/log'
import { HotkeysService } from 'services/hotkeys' import { HotkeysService } from 'services/hotkeys'
import { ModalService } from 'services/modal' import { ModalService } from 'services/modal'
import { NotifyService } from 'services/notify' import { NotifyService } from 'services/notify'
import { PluginDispatcherService } from 'services/pluginDispatcher'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { SessionsService } from 'services/sessions' import { SessionsService } from 'services/sessions'
import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter'
@ -42,6 +43,7 @@ import { TerminalComponent } from 'components/terminal'
LogService, LogService,
ModalService, ModalService,
NotifyService, NotifyService,
PluginDispatcherService,
QuitterService, QuitterService,
SessionsService, SessionsService,
LocalStorageService, LocalStorageService,
@ -63,4 +65,8 @@ import { TerminalComponent } from 'components/terminal'
AppComponent AppComponent
] ]
}) })
export class AppModule {} export class AppModule {
constructor (pluginDispatcher: PluginDispatcherService) {
pluginDispatcher.register(require('./plugin.hyperlinks').default)
}
}

View File

@ -1,5 +1,6 @@
@import "~bootstrap/less/variables.less"; @import "~variables.less";
@import "~bootstrap/variables.less"; @import "~mixins.less";
:host { :host {
display: flex; display: flex;
@ -17,20 +18,6 @@
@tabs-height: 40px; @tabs-height: 40px;
@tab-border-radius: 4px; @tab-border-radius: 4px;
.button-states() {
transition: 0.125s all;
border: none;
background: transparent;
&:hover:not(.active) {
background: rgba(255, 255, 255, .033);
}
&:active:not(.active) {
background: rgba(0, 0, 0, .1);
}
}
.titlebar { .titlebar {
height: @titlebar-height; height: @titlebar-height;
background: #141c23; background: #141c23;
@ -50,8 +37,15 @@
line-height: @titlebar-height - 2px; line-height: @titlebar-height - 2px;
padding: 0 15px; padding: 0 15px;
font-size: 8px; font-size: 8px;
color: #444;
background: transparent;
transition: 0.25s all;
.button-states(); .button-states();
&:hover {
color: white;
}
cursor: pointer; cursor: pointer;
} }
@ -138,8 +132,11 @@
border: none; border: none;
background: transparent; background: transparent;
opacity: 0; color: @text-color;
transition: 0.25s all; transition: 0.25s all;
display: block;
opacity: 0;
@button-size: @tabs-height * 0.6; @button-size: @tabs-height * 0.6;
width: @button-size; width: @button-size;
@ -149,7 +146,6 @@
margin-top: (@tabs-height - @button-size) * 0.4; margin-top: (@tabs-height - @button-size) * 0.4;
margin-right: 10px; margin-right: 10px;
display: block;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
@ -163,6 +159,8 @@
} }
&:hover button { &:hover button {
transition: 0.25s opacity;
display: block;
opacity: 1; opacity: 1;
} }
} }

View File

@ -81,6 +81,12 @@ export class AppComponent {
if (hotkey == 'new-tab') { if (hotkey == 'new-tab') {
this.newTab() this.newTab()
} }
if (hotkey.startsWith('tab-')) {
let index = parseInt(hotkey.split('-')[1])
if (index <= this.tabs.length) {
this.selectTab(this.tabs[index - 1])
}
}
if (this.activeTab) { if (this.activeTab) {
if (hotkey == 'close-tab') { if (hotkey == 'close-tab') {
this.closeTab(this.activeTab) this.closeTab(this.activeTab)
@ -137,7 +143,10 @@ export class AppComponent {
} }
this.activeTab = tab this.activeTab = tab
setImmediate(() => { setImmediate(() => {
this.elementRef.nativeElement.querySelector(':scope .tab.active iframe').focus() let iframe = this.elementRef.nativeElement.querySelector(':scope .tab.active iframe')
if (iframe) {
iframe.focus()
}
}) })
} }

View File

@ -1,4 +1,4 @@
import { Component, Input, ChangeDetectionStrategy, trigger, style, animate, transition, state } from '@angular/core' import { Component, Input, trigger, style, animate, transition } from '@angular/core'
@Component({ @Component({

View File

@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, trigger, style, animate, transition, state } from '@angular/core' import { Component, Input, trigger, style, animate, transition, state } from '@angular/core'
import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys' import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys'
@ -33,7 +33,7 @@ import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys'
}) })
export class HotkeyHintComponent { export class HotkeyHintComponent {
@Input() partialHotkeyMatches: PartialHotkeyMatch[] @Input() partialHotkeyMatches: PartialHotkeyMatch[]
private keyTimeoutInterval: NodeJS.Timer = null private keyTimeoutInterval: number = null
constructor ( constructor (
public hotkeys: HotkeysService, public hotkeys: HotkeysService,

View File

@ -14,7 +14,7 @@ const INPUT_TIMEOUT = 2000
export class HotkeyInputModalComponent { export class HotkeyInputModalComponent {
private keySubscription: Subscription private keySubscription: Subscription
private lastKeyEvent: number private lastKeyEvent: number
private keyTimeoutInterval: NodeJS.Timer private keyTimeoutInterval: number = null
@Input() value: string[] = [] @Input() value: string[] = []
@Input() timeoutProgress = 0 @Input() timeoutProgress = 0

View File

@ -1,4 +1,6 @@
:host { :host {
overflow-y: auto;
>.modal-body { >.modal-body {
padding: 0 0 20px !important; padding: 0 0 20px !important;
} }

View File

@ -1,7 +1,13 @@
ngb-tabset(type='tabs nav-justified') ngb-tabset(type='tabs')
ngb-tab
template(ngbTabTitle)
| General
template(ngbTabContent)
.form-group
label Font
input.form-control(type='text', [ngbTypeahead]='fontAutocomplete', '[(ngModel)]'='font')
ngb-tab ngb-tab
template(ngbTabTitle) template(ngbTabTitle)
i.fa.fa-keyboard-o
| Hotkeys | Hotkeys
template(ngbTabContent) template(ngbTabContent)
.form-group .form-group

View File

@ -2,6 +2,11 @@ import { Component } from '@angular/core'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp' import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/distinctUntilChanged'
const childProcessPromise = nodeRequire('child-process-promise')
@Component({ @Component({
@ -27,9 +32,29 @@ export class SettingsPaneComponent {
isLinux: boolean isLinux: boolean
year: number year: number
version: string version: string
fonts: string[] = []
globalHotkey = ['Ctrl+Shift+G'] globalHotkey = ['Ctrl+Shift+G']
ngOnInit () {
childProcessPromise.exec('fc-list :spacing=mono').then((result) => {
this.fonts = result.stdout
.split('\n')
.filter((x) => !!x)
.map((x) => x.split(':')[1].trim())
.map((x) => x.split(',')[0].trim())
this.fonts.sort()
})
}
fontAutocomplete = (text$: Observable<string>) => {
return text$
.debounceTime(200)
.distinctUntilChanged()
.map(query => this.fonts.filter(v => new RegExp(query, 'gi').test(v)))
.map(list => Array.from(new Set(list)))
}
ngOnDestroy() { ngOnDestroy() {
this.config.save() this.config.save()
} }

View File

@ -1,9 +1,11 @@
import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core' import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { PluginDispatcherService } from 'services/pluginDispatcher'
import { Session } from 'services/sessions' import { Session } from 'services/sessions'
const hterm = require('hterm-commonjs') const hterm = require('hterm-commonjs')
const dataurl = require('dataurl')
hterm.hterm.VT.ESC['k'] = function(parseState) { hterm.hterm.VT.ESC['k'] = function(parseState) {
@ -21,7 +23,19 @@ hterm.hterm.VT.ESC['k'] = function(parseState) {
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
const pmgr = new hterm.hterm.PreferenceManager('default') const pmgr = new hterm.hterm.PreferenceManager('default')
pmgr.set('user-css', ``) pmgr.set('user-css', dataurl.convert({
data: `
a {
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
`,
mimetype: 'text/css',
charset: 'utf8',
}))
pmgr.set('font-size', 12) pmgr.set('font-size', 12)
pmgr.set('background-color', '#1D272D') pmgr.set('background-color', '#1D272D')
pmgr.set('color-palette-overrides', { pmgr.set('color-palette-overrides', {
@ -43,18 +57,20 @@ export class TerminalComponent {
@Input() session: Session @Input() session: Session
title: string title: string
@Output() titleChange = new EventEmitter() @Output() titleChange = new EventEmitter()
private terminal: any terminal: any
constructor( constructor(
private zone: NgZone, private zone: NgZone,
private elementRef: ElementRef, private elementRef: ElementRef,
public config: ConfigService, public config: ConfigService,
private pluginDispatcher: PluginDispatcherService,
) { ) {
} }
ngOnInit () { ngOnInit () {
let io let io
this.terminal = new hterm.hterm.Terminal() this.terminal = new hterm.hterm.Terminal()
this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal })
this.terminal.setWindowTitle = (title) => { this.terminal.setWindowTitle = (title) => {
this.zone.run(() => { this.zone.run(() => {
this.title = title this.title = title
@ -83,6 +99,7 @@ export class TerminalComponent {
this.session.releaseInitialDataBuffer() this.session.releaseInitialDataBuffer()
} }
this.terminal.decorate(this.elementRef.nativeElement) this.terminal.decorate(this.elementRef.nativeElement)
this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal })
} }
ngOnDestroy () { ngOnDestroy () {

View File

@ -1,6 +1,3 @@
import 'source-sans-pro' import 'source-sans-pro'
import 'font-awesome/css/font-awesome.css' import 'font-awesome/css/font-awesome.css'
import '../assets/toaster-custom.less' import '../assets/toaster-custom.less'
import '../assets/bootstrap/bootstrap.less'

View File

@ -1,5 +1,16 @@
@import "~bootstrap/include.less"; @import "~variables.less";
@import "~mixins.less";
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: @font-family;
font-size: @font-size;
color: @text-color;
}
html.platform-win32 { html.platform-win32 {
body.focused { body.focused {
@ -85,25 +96,37 @@ ngb-modal-window.fade.in {
} }
ngb-tabset { ngb-tabset {
>ul.nav-tabs.nav-justified { >ul.nav-tabs {
border-bottom: none; border-bottom: none;
margin-bottom: 10px; margin-bottom: 10px;
background: rgba(0,0,0,.25); background: rgba(0,0,0,.25);
display: flex;
align-items: start;
padding: 0;
margin: 0;
.nav-item .nav-link { .nav-item {
background: transparent; flex: none;
border: none; display: flex;
&.active { .nav-link {
background: rgba(0,0,0,.5); background: transparent;
border-bottom: 1px solid #777; border: none;
} padding: 10px 15px;
color: @text-color;
text-decoration: none;
i { &.active {
display: block; background: rgba(0,0,0,.5);
text-align: center; border-bottom: 1px solid #777;
font-size: 18px; }
margin: 0 0 5px;
i {
display: block;
text-align: center;
font-size: 18px;
margin: 0 0 5px;
}
} }
} }
} }
@ -134,3 +157,21 @@ ngb-tabset {
} }
} }
} }
ngb-typeahead-window {
max-height: 200px;
overflow-y: auto;
>button {
display: block;
width: 100%;
-webkit-appearance: none;
border-bottom: 1px solid @dark-border;
.list-group-item-style();
}
}
.list-group-item {
.list-group-item-style();
}

24
app/src/mixins.less Normal file
View File

@ -0,0 +1,24 @@
@import "~variables.less";
.button-states() {
transition: 0.125s all;
border: none;
&:hover:not(.active) {
background: rgba(255, 255, 255, .033);
}
&:active:not(.active),
&.active {
background: rgba(0, 0, 0, .1);
}
}
.list-group-item-style() {
display: block;
padding: 10px 15px;
background: @component-bg;
color: @text-color;
text-align: left;
.button-states();
}

View File

@ -0,0 +1,123 @@
import * as fs from 'fs'
import { ElectronService } from 'services/electron'
abstract class Handler {
constructor (protected plugin) { }
regex: string
convert (uri: string): string { return uri }
verify (_uri: string): boolean { return true }
abstract handle (uri: string): void
}
class URLHandler extends Handler {
regex = 'http(s)?://[^\\s;\'"]+[^.,;\\s]'
handle (uri: string) {
this.plugin.electron.shell.openExternal(uri)
}
}
class FileHandler extends Handler {
regex = '/[^\\s.,;\'"]+'
verify (uri: string) {
return fs.existsSync(uri)
}
handle (uri: string) {
this.plugin.electron.shell.openExternal('file://' + uri)
}
}
export default class HyperlinksPlugin {
handlers = []
handlerClasses = [
URLHandler,
FileHandler,
]
electron: ElectronService
constructor ({ electron }) {
this.electron = electron
this.handlers = this.handlerClasses.map((x) => new x(this))
}
preTerminalInit ({ terminal }) {
const oldInsertString = terminal.screen_.constructor.prototype.insertString
const oldDeleteChars = terminal.screen_.constructor.prototype.deleteChars
terminal.screen_.insertString = (...args) => {
let ret = oldInsertString.bind(terminal.screen_)(...args)
this.insertLinks(terminal.screen_)
return ret
}
terminal.screen_.deleteChars = (...args) => {
let ret = oldDeleteChars.bind(terminal.screen_)(...args)
this.insertLinks(terminal.screen_)
return ret
}
}
insertLinks (screen) {
const traverse = (element) => {
Array.from(element.childNodes).forEach((node) => {
if (node.nodeName == '#text') {
element.replaceChild(this.urlizeNode(node), node)
} else if (node.nodeName != 'A') {
traverse(node)
}
})
}
screen.rowsArray.forEach((x) => traverse(x))
}
urlizeNode (node) {
let matches = []
this.handlers.forEach((handler) => {
let regex = new RegExp(handler.regex, 'gi')
let match
while (match = regex.exec(node.textContent)) {
let uri = handler.convert(match[0])
if (!handler.verify(uri)) {
continue;
}
matches.push({
start: regex.lastIndex - match[0].length,
end: regex.lastIndex,
text: match[0],
uri,
handler
})
}
})
if (matches.length == 0) {
return node
}
matches.sort((a, b) => a.start < b.start ? -1 : 1)
let span = document.createElement('span')
let position = 0
matches.forEach((match) => {
if (match.start < position) {
return
}
if (match.start > position) {
span.appendChild(document.createTextNode(node.textContent.slice(position, match.start)))
}
let a = document.createElement('a')
a.textContent = match.text
a.addEventListener('click', () => {
match.handler.handle(match.uri)
})
span.appendChild(a)
position = match.end
})
span.appendChild(document.createTextNode(node.textContent.slice(position)))
return span
}
}

View File

@ -1,40 +1,40 @@
import * as yaml from 'js-yaml'
import * as path from 'path'
import * as fs from 'fs'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
const Config = nodeRequire('electron-config') import { ElectronService } from 'services/electron'
const defaultConfig : IConfigData = require('../../defaultConfig.yaml')
export interface IConfigData {
hotkeys?: any
}
@Injectable() @Injectable()
export class ConfigService { export class ConfigService {
constructor() { constructor (
this.config = new Config({name: 'config'}) electron: ElectronService
) {
this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
this.load() this.load()
} }
private config: any private path: string
private store: any private store: IConfigData
set(key: string, value: any) { load () {
this.store.set(key, value) if (fs.existsSync(this.path)) {
this.save() this.store = yaml.safeLoad(fs.readFileSync(this.path, 'utf8'))
} else {
this.store = {}
}
} }
get(key: string): any { save () {
return this.store[key] fs.writeFileSync(this.path, yaml.safeDump(this.store), 'utf8')
} }
has(key: string): boolean { full () : IConfigData {
return this.store[key] != undefined return Object.assign({}, defaultConfig, this.store)
}
delete(key: string) {
delete this.store[key]
this.save()
}
load() {
this.store = this.config.store
}
save() {
this.config.store = this.store
} }
} }

View File

@ -7,7 +7,6 @@ const hterm = require('hterm-commonjs')
export interface HotkeyDescription { export interface HotkeyDescription {
id: string, id: string,
name: string, name: string,
defaults: string[][],
} }
export interface PartialHotkeyMatch { export interface PartialHotkeyMatch {
@ -21,27 +20,62 @@ const HOTKEYS: HotkeyDescription[] = [
{ {
id: 'new-tab', id: 'new-tab',
name: 'New tab', name: 'New tab',
defaults: [['Ctrl+Shift+T'], ['Ctrl+A', 'C']],
}, },
{ {
id: 'close-tab', id: 'close-tab',
name: 'Close tab', name: 'Close tab',
defaults: [['Ctrl+Shift+W'], ['Ctrl+A', 'K']],
}, },
{ {
id: 'toggle-last-tab', id: 'toggle-last-tab',
name: 'Toggle last tab', name: 'Toggle last tab',
defaults: [['Ctrl+A', 'A'], ['Ctrl+A', 'Ctrl+A']],
}, },
{ {
id: 'next-tab', id: 'next-tab',
name: 'Next tab', name: 'Next tab',
defaults: [['Ctrl+Shift-ArrowRight'], ['Ctrl+A', 'N']],
}, },
{ {
id: 'previous-tab', id: 'previous-tab',
name: 'Previous tab', name: 'Previous tab',
defaults: [['Ctrl+Shift-ArrowLeft'], ['Ctrl+A', 'P']], },
{
id: 'tab-1',
name: 'Tab 1',
},
{
id: 'tab-2',
name: 'Tab 2',
},
{
id: 'tab-3',
name: 'Tab 3',
},
{
id: 'tab-4',
name: 'Tab 4',
},
{
id: 'tab-5',
name: 'Tab 5',
},
{
id: 'tab-6',
name: 'Tab 6',
},
{
id: 'tab-7',
name: 'Tab 7',
},
{
id: 'tab-8',
name: 'Tab 8',
},
{
id: 'tab-9',
name: 'Tab 9',
},
{
id: 'tab-10',
name: 'Tab 10',
}, },
] ]
@ -86,10 +120,6 @@ export class HotkeysService {
oldHandler.bind(this)(nativeEvent) oldHandler.bind(this)(nativeEvent)
} }
}) })
if (!config.get('hotkeys')) {
config.set('hotkeys', {})
}
} }
emitNativeEvent (name, nativeEvent) { emitNativeEvent (name, nativeEvent) {
@ -122,18 +152,21 @@ export class HotkeysService {
registerHotkeys () { registerHotkeys () {
this.electron.globalShortcut.unregisterAll() this.electron.globalShortcut.unregisterAll()
this.electron.globalShortcut.register('`', () => { // TODO
this.electron.globalShortcut.register('PrintScreen', () => {
this.globalHotkey.emit() this.globalHotkey.emit()
}) })
} }
getHotkeysConfig () { getHotkeysConfig () {
let keys = {} let keys = {}
for (let key of HOTKEYS) { for (let key in this.config.full().hotkeys) {
keys[key.id] = key.defaults let value = this.config.full().hotkeys[key]
} if (typeof value == 'string') {
for (let key in this.config.get('hotkeys')) { value = [value]
keys[key] = this.config.get('hotkeys')[key] }
value = value.map((item) => (typeof item == 'string') ? [item] : item)
keys[key] = value
} }
return keys return keys
} }

View File

@ -52,7 +52,7 @@ export function stringifyKeySequence(events: NativeKeyEvent[]): string[] {
continue continue
} }
itemKeys.push(lastEvent.key) itemKeys.push(lastEvent.key)
items.push(itemKeys.join('+')) items.push(itemKeys.join('-'))
} }
lastEvent = event lastEvent = event
} }

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core'
import { ConfigService } from 'services/config'
import { ElectronService } from 'services/electron'
@Injectable()
export class PluginDispatcherService {
plugins = []
constructor (
private config: ConfigService,
private electron: ElectronService,
) {
}
register (plugin) {
if (!this.plugins.includes(plugin)) {
this.plugins.push(new plugin({
config: this.config,
electron: this.electron,
}))
}
}
emit (event: string, parameters: any) {
this.plugins.forEach((plugin) => {
if (plugin[event]) {
plugin[event].bind(plugin)(parameters)
}
})
}
}

16
app/src/variables.less Normal file
View File

@ -0,0 +1,16 @@
@brand-primary: #f7e61d;
@brand-success: #42B500;
@brand-info: #01BAEF;
@brand-warning: #DB8A00;
@brand-danger: #EF2F00;
@body-bg: #1D272D;
@text-color: #aaa;
@font-family: "Source Sans Pro";
@font-size: 14px;
@dark-border: rgba(0,0,0,.25);
@light-border: rgba(255,255,255,.25);
@component-bg: #161d21;

107
npm-debug.log Normal file
View File

@ -0,0 +1,107 @@
0 info it worked if it ends with ok
1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'i', '-D', '@types/data-url' ]
2 info using npm@4.1.2
3 info using node@v7.6.0
4 silly loadCurrentTree Starting
5 silly install loadCurrentTree
6 silly install readLocalPackageData
7 silly fetchPackageMetaData @types/data-url
8 silly fetchNamedPackageData @types/data-url
9 silly mapToRegistry name @types/data-url
10 silly mapToRegistry scope (from package name) @types
11 verbose mapToRegistry no registry URL found in name for scope @types
12 silly mapToRegistry using default registry
13 silly mapToRegistry registry https://registry.npmjs.org/
14 silly mapToRegistry data Result {
14 silly mapToRegistry raw: '@types/data-url',
14 silly mapToRegistry scope: '@types',
14 silly mapToRegistry escapedName: '@types%2fdata-url',
14 silly mapToRegistry name: '@types/data-url',
14 silly mapToRegistry rawSpec: '',
14 silly mapToRegistry spec: 'latest',
14 silly mapToRegistry type: 'tag' }
15 silly mapToRegistry uri https://registry.npmjs.org/@types%2fdata-url
16 verbose request uri https://registry.npmjs.org/@types%2fdata-url
17 verbose request no auth needed
18 info attempt registry request try #1 at 5:03:52 PM
19 verbose request using bearer token for auth
20 verbose request id 5b615716d245b7ec
21 http request GET https://registry.npmjs.org/@types%2fdata-url
22 http 404 https://registry.npmjs.org/@types%2fdata-url
23 verbose headers { 'content-type': 'application/json',
23 verbose headers 'cache-control': 'max-age=0',
23 verbose headers 'content-length': '2',
23 verbose headers 'accept-ranges': 'bytes',
23 verbose headers date: 'Sun, 05 Mar 2017 16:03:53 GMT',
23 verbose headers via: '1.1 varnish',
23 verbose headers connection: 'keep-alive',
23 verbose headers 'x-served-by': 'cache-hhn1546-HHN',
23 verbose headers 'x-cache': 'MISS',
23 verbose headers 'x-cache-hits': '0',
23 verbose headers 'x-timer': 'S1488729833.041564,VS0,VE332',
23 verbose headers vary: 'Accept-Encoding' }
24 silly get cb [ 404,
24 silly get { 'content-type': 'application/json',
24 silly get 'cache-control': 'max-age=0',
24 silly get 'content-length': '2',
24 silly get 'accept-ranges': 'bytes',
24 silly get date: 'Sun, 05 Mar 2017 16:03:53 GMT',
24 silly get via: '1.1 varnish',
24 silly get connection: 'keep-alive',
24 silly get 'x-served-by': 'cache-hhn1546-HHN',
24 silly get 'x-cache': 'MISS',
24 silly get 'x-cache-hits': '0',
24 silly get 'x-timer': 'S1488729833.041564,VS0,VE332',
24 silly get vary: 'Accept-Encoding' } ]
25 silly fetchPackageMetaData Error: Registry returned 404 for GET on https://registry.npmjs.org/@types%2fdata-url
25 silly fetchPackageMetaData at makeError (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:304:12)
25 silly fetchPackageMetaData at CachingRegistryClient.<anonymous> (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:282:14)
25 silly fetchPackageMetaData at Request._callback (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:212:14)
25 silly fetchPackageMetaData at Request.self.callback (/usr/lib/node_modules/npm/node_modules/request/request.js:186:22)
25 silly fetchPackageMetaData at emitTwo (events.js:106:13)
25 silly fetchPackageMetaData at Request.emit (events.js:192:7)
25 silly fetchPackageMetaData at Request.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1081:10)
25 silly fetchPackageMetaData at emitOne (events.js:96:13)
25 silly fetchPackageMetaData at Request.emit (events.js:189:7)
25 silly fetchPackageMetaData at IncomingMessage.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1001:12)
25 silly fetchPackageMetaData error for @types/data-url { Error: Registry returned 404 for GET on https://registry.npmjs.org/@types%2fdata-url
25 silly fetchPackageMetaData at makeError (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:304:12)
25 silly fetchPackageMetaData at CachingRegistryClient.<anonymous> (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:282:14)
25 silly fetchPackageMetaData at Request._callback (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:212:14)
25 silly fetchPackageMetaData at Request.self.callback (/usr/lib/node_modules/npm/node_modules/request/request.js:186:22)
25 silly fetchPackageMetaData at emitTwo (events.js:106:13)
25 silly fetchPackageMetaData at Request.emit (events.js:192:7)
25 silly fetchPackageMetaData at Request.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1081:10)
25 silly fetchPackageMetaData at emitOne (events.js:96:13)
25 silly fetchPackageMetaData at Request.emit (events.js:189:7)
25 silly fetchPackageMetaData at IncomingMessage.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1001:12) pkgid: '@types/data-url', statusCode: 404, code: 'E404' }
26 silly rollbackFailedOptional Starting
27 silly rollbackFailedOptional Finishing
28 silly runTopLevelLifecycles Finishing
29 silly install printInstalled
30 verbose stack Error: Registry returned 404 for GET on https://registry.npmjs.org/@types%2fdata-url
30 verbose stack at makeError (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:304:12)
30 verbose stack at CachingRegistryClient.<anonymous> (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:282:14)
30 verbose stack at Request._callback (/usr/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:212:14)
30 verbose stack at Request.self.callback (/usr/lib/node_modules/npm/node_modules/request/request.js:186:22)
30 verbose stack at emitTwo (events.js:106:13)
30 verbose stack at Request.emit (events.js:192:7)
30 verbose stack at Request.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1081:10)
30 verbose stack at emitOne (events.js:96:13)
30 verbose stack at Request.emit (events.js:189:7)
30 verbose stack at IncomingMessage.<anonymous> (/usr/lib/node_modules/npm/node_modules/request/request.js:1001:12)
31 verbose statusCode 404
32 verbose pkgid @types/data-url
33 verbose cwd /home/eugene/Work/term
34 error Linux 4.8.0-39-generic
35 error argv "/usr/bin/nodejs" "/usr/bin/npm" "i" "-D" "@types/data-url"
36 error node v7.6.0
37 error npm v4.1.2
38 error code E404
39 error 404 Registry returned 404 for GET on https://registry.npmjs.org/@types%2fdata-url
40 error 404
41 error 404 '@types/data-url' is not in the npm registry.
42 error 404 You should bug the author to publish it (or use the name yourself!)
43 error 404 Note that you can also install from a
44 error 404 tarball, folder, http url, or git url.
45 verbose exit [ 1, true ]

View File

@ -2,8 +2,9 @@
"name": "term", "name": "term",
"devDependencies": { "devDependencies": {
"apply-loader": "^0.1.0", "apply-loader": "^0.1.0",
"awesome-typescript-loader": "2.2.4", "awesome-typescript-loader": "3.0.8",
"css-loader": "0.26.1", "css-loader": "0.26.1",
"dataurl": "^0.1.0",
"electron": "^1.4.13", "electron": "^1.4.13",
"electron-builder": "10.6.1", "electron-builder": "10.6.1",
"electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04", "electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04",
@ -11,6 +12,7 @@
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"html-loader": "^0.4.4", "html-loader": "^0.4.4",
"json-loader": "^0.5.4",
"less": "^2.7.1", "less": "^2.7.1",
"less-loader": "^2.2.3", "less-loader": "^2.2.3",
"node-gyp": "^3.4.0", "node-gyp": "^3.4.0",
@ -21,11 +23,11 @@
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"to-string-loader": "^1.1.5", "to-string-loader": "^1.1.5",
"tslint": "4.2.0", "tslint": "4.2.0",
"typescript": "2.1.1", "typescript": "2.2.1",
"typings": "2.0.0",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"val-loader": "^0.5.0", "val-loader": "^0.5.0",
"webpack": "2.2.0-rc.4" "webpack": "2.2.0-rc.4",
"yaml-loader": "^0.4.0"
}, },
"build": { "build": {
"appId": "com.elements.benchmark", "appId": "com.elements.benchmark",
@ -62,15 +64,20 @@
"@angular/platform-server": "2.3.1", "@angular/platform-server": "2.3.1",
"@angular/router": "3.3.1", "@angular/router": "3.3.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.15", "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.15",
"@types/core-js": "^0.9.35",
"@types/electron": "^1.4.33",
"@types/js-yaml": "^3.5.29",
"@types/node": "^7.0.5",
"@types/pty.js": "^0.2.32",
"angular2-localstorage": "github:AilisObrian/angular2-localstorage", "angular2-localstorage": "github:AilisObrian/angular2-localstorage",
"angular2-perfect-scrollbar": "^1.1.0", "angular2-perfect-scrollbar": "^1.1.0",
"angular2-toaster": "^1.1.0", "angular2-toaster": "^1.1.0",
"bootstrap": "^3.3.7", "bootstrap": "^3.3.7",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"hterm-commonjs": "^1.0.0",
"jquery": "^3.1.1", "jquery": "^3.1.1",
"rxjs": "5.0.0-rc.4", "rxjs": "5.0.0-rc.4",
"source-sans-pro": "^2.0.10", "source-sans-pro": "^2.0.10",
"hterm-commonjs": "^1.0.0",
"zone.js": "0.7.2" "zone.js": "0.7.2"
} }
} }

View File

@ -1,11 +0,0 @@
{
"globalDependencies": {
"core-js": "registry:dt/core-js#0.0.0+20160914114559",
"electron": "registry:dt/electron#1.3.3+20161012142539",
"jquery": "registry:dt/jquery#1.10.0+20160929162922",
"node": "registry:dt/node#6.0.0+20161014191813"
},
"dependencies": {
"pty.js": "registry:dt/pty.js#0.2.7-1+20161128184045"
}
}

View File

@ -64,6 +64,7 @@ module.exports = {
name: 'fonts/[name].[hash:8].[ext]' name: 'fonts/[name].[hash:8].[ext]'
} }
}, },
{ test: /\.yaml$/, loader: "json-loader!yaml-loader" },
] ]
}, },
externals: { externals: {