diff --git a/Makefile b/Makefile index e23198d5..925c34d1 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ SHORT_VERSION=$(shell python -c 'import subprocess; v = subprocess.check_output( all: run run: - DEV=1 ./node_modules/.bin/electron ./app + DEV=1 ./node_modules/.bin/electron ./app --debug lint: tslint -c tslint.json app/src/*.ts app/src/**/*.ts diff --git a/app/assets/bootstrap/variables.less b/app/assets/bootstrap/variables.less deleted file mode 100644 index 66e9a726..00000000 --- a/app/assets/bootstrap/variables.less +++ /dev/null @@ -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; diff --git a/app/defaultConfig.yaml b/app/defaultConfig.yaml new file mode 100644 index 00000000..5c92c8aa --- /dev/null +++ b/app/defaultConfig.yaml @@ -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'] diff --git a/app/package.json b/app/package.json index 1157064b..7f4fdb6d 100644 --- a/app/package.json +++ b/app/package.json @@ -10,5 +10,8 @@ "electron-is-dev": "^0.1.2", "path": "^0.12.7", "pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642" + }, + "devDependencies": { + "js-yaml": "^3.8.2" } } diff --git a/app/src/app.module.ts b/app/src/app.module.ts index baac46c6..aac7f91c 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -12,6 +12,7 @@ import { LogService } from 'services/log' import { HotkeysService } from 'services/hotkeys' import { ModalService } from 'services/modal' import { NotifyService } from 'services/notify' +import { PluginDispatcherService } from 'services/pluginDispatcher' import { QuitterService } from 'services/quitter' import { SessionsService } from 'services/sessions' import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' @@ -42,6 +43,7 @@ import { TerminalComponent } from 'components/terminal' LogService, ModalService, NotifyService, + PluginDispatcherService, QuitterService, SessionsService, LocalStorageService, @@ -63,4 +65,8 @@ import { TerminalComponent } from 'components/terminal' AppComponent ] }) -export class AppModule {} +export class AppModule { + constructor (pluginDispatcher: PluginDispatcherService) { + pluginDispatcher.register(require('./plugin.hyperlinks').default) + } +} diff --git a/app/src/components/app.less b/app/src/components/app.less index 3755adc3..2cbed8d1 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -1,5 +1,6 @@ -@import "~bootstrap/less/variables.less"; -@import "~bootstrap/variables.less"; +@import "~variables.less"; +@import "~mixins.less"; + :host { display: flex; @@ -17,20 +18,6 @@ @tabs-height: 40px; @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 { height: @titlebar-height; background: #141c23; @@ -50,8 +37,15 @@ line-height: @titlebar-height - 2px; padding: 0 15px; font-size: 8px; + color: #444; + background: transparent; + transition: 0.25s all; .button-states(); + + &:hover { + color: white; + } cursor: pointer; } @@ -138,8 +132,11 @@ border: none; background: transparent; - opacity: 0; + color: @text-color; + transition: 0.25s all; + display: block; + opacity: 0; @button-size: @tabs-height * 0.6; width: @button-size; @@ -149,7 +146,6 @@ margin-top: (@tabs-height - @button-size) * 0.4; margin-right: 10px; - display: block; text-align: center; font-size: 20px; @@ -163,6 +159,8 @@ } &:hover button { + transition: 0.25s opacity; + display: block; opacity: 1; } } diff --git a/app/src/components/app.ts b/app/src/components/app.ts index 2795aa15..7d416c09 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -81,6 +81,12 @@ export class AppComponent { if (hotkey == 'new-tab') { 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 (hotkey == 'close-tab') { this.closeTab(this.activeTab) @@ -137,7 +143,10 @@ export class AppComponent { } this.activeTab = tab setImmediate(() => { - this.elementRef.nativeElement.querySelector(':scope .tab.active iframe').focus() + let iframe = this.elementRef.nativeElement.querySelector(':scope .tab.active iframe') + if (iframe) { + iframe.focus() + } }) } diff --git a/app/src/components/hotkeyDisplay.ts b/app/src/components/hotkeyDisplay.ts index 91b09fc6..2b9b48d6 100644 --- a/app/src/components/hotkeyDisplay.ts +++ b/app/src/components/hotkeyDisplay.ts @@ -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({ diff --git a/app/src/components/hotkeyHint.ts b/app/src/components/hotkeyHint.ts index 4f461d1b..769cb1dd 100644 --- a/app/src/components/hotkeyHint.ts +++ b/app/src/components/hotkeyHint.ts @@ -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' @@ -33,7 +33,7 @@ import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys' }) export class HotkeyHintComponent { @Input() partialHotkeyMatches: PartialHotkeyMatch[] - private keyTimeoutInterval: NodeJS.Timer = null + private keyTimeoutInterval: number = null constructor ( public hotkeys: HotkeysService, diff --git a/app/src/components/hotkeyInputModal.ts b/app/src/components/hotkeyInputModal.ts index 0b8c6c19..c4198243 100644 --- a/app/src/components/hotkeyInputModal.ts +++ b/app/src/components/hotkeyInputModal.ts @@ -14,7 +14,7 @@ const INPUT_TIMEOUT = 2000 export class HotkeyInputModalComponent { private keySubscription: Subscription private lastKeyEvent: number - private keyTimeoutInterval: NodeJS.Timer + private keyTimeoutInterval: number = null @Input() value: string[] = [] @Input() timeoutProgress = 0 diff --git a/app/src/components/settingsPane.less b/app/src/components/settingsPane.less index 9b22a4db..c6f9a7f4 100644 --- a/app/src/components/settingsPane.less +++ b/app/src/components/settingsPane.less @@ -1,4 +1,6 @@ :host { + overflow-y: auto; + >.modal-body { padding: 0 0 20px !important; } diff --git a/app/src/components/settingsPane.pug b/app/src/components/settingsPane.pug index 8d5e9ca7..2704fe20 100644 --- a/app/src/components/settingsPane.pug +++ b/app/src/components/settingsPane.pug @@ -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 template(ngbTabTitle) - i.fa.fa-keyboard-o | Hotkeys template(ngbTabContent) .form-group diff --git a/app/src/components/settingsPane.ts b/app/src/components/settingsPane.ts index 3e6d9f0d..46aaabaa 100644 --- a/app/src/components/settingsPane.ts +++ b/app/src/components/settingsPane.ts @@ -2,6 +2,11 @@ import { Component } from '@angular/core' import { ElectronService } from 'services/electron' import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp' 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({ @@ -27,9 +32,29 @@ export class SettingsPaneComponent { isLinux: boolean year: number version: string + fonts: string[] = [] 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) => { + 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() { this.config.save() } diff --git a/app/src/components/terminal.ts b/app/src/components/terminal.ts index 6a2bb5c4..d7d80a94 100644 --- a/app/src/components/terminal.ts +++ b/app/src/components/terminal.ts @@ -1,9 +1,11 @@ import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core' import { ConfigService } from 'services/config' +import { PluginDispatcherService } from 'services/pluginDispatcher' import { Session } from 'services/sessions' const hterm = require('hterm-commonjs') +const dataurl = require('dataurl') 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() 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('background-color', '#1D272D') pmgr.set('color-palette-overrides', { @@ -43,18 +57,20 @@ export class TerminalComponent { @Input() session: Session title: string @Output() titleChange = new EventEmitter() - private terminal: any + terminal: any constructor( private zone: NgZone, private elementRef: ElementRef, public config: ConfigService, + private pluginDispatcher: PluginDispatcherService, ) { } ngOnInit () { let io this.terminal = new hterm.hterm.Terminal() + this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal }) this.terminal.setWindowTitle = (title) => { this.zone.run(() => { this.title = title @@ -83,6 +99,7 @@ export class TerminalComponent { this.session.releaseInitialDataBuffer() } this.terminal.decorate(this.elementRef.nativeElement) + this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal }) } ngOnDestroy () { diff --git a/app/src/entry.preload.ts b/app/src/entry.preload.ts index 46839062..3a8cf095 100644 --- a/app/src/entry.preload.ts +++ b/app/src/entry.preload.ts @@ -1,6 +1,3 @@ import 'source-sans-pro' - import 'font-awesome/css/font-awesome.css' - import '../assets/toaster-custom.less' -import '../assets/bootstrap/bootstrap.less' diff --git a/app/src/global.less b/app/src/global.less index 3edc6c73..829b3644 100644 --- a/app/src/global.less +++ b/app/src/global.less @@ -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 { body.focused { @@ -85,25 +96,37 @@ ngb-modal-window.fade.in { } ngb-tabset { - >ul.nav-tabs.nav-justified { + >ul.nav-tabs { border-bottom: none; margin-bottom: 10px; background: rgba(0,0,0,.25); + display: flex; + align-items: start; + padding: 0; + margin: 0; - .nav-item .nav-link { - background: transparent; - border: none; + .nav-item { + flex: none; + display: flex; - &.active { - background: rgba(0,0,0,.5); - border-bottom: 1px solid #777; - } + .nav-link { + background: transparent; + border: none; + padding: 10px 15px; + color: @text-color; + text-decoration: none; - i { - display: block; - text-align: center; - font-size: 18px; - margin: 0 0 5px; + &.active { + background: rgba(0,0,0,.5); + border-bottom: 1px solid #777; + } + + 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(); +} diff --git a/app/src/mixins.less b/app/src/mixins.less new file mode 100644 index 00000000..7acb1d67 --- /dev/null +++ b/app/src/mixins.less @@ -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(); +} diff --git a/app/src/plugin.hyperlinks.ts b/app/src/plugin.hyperlinks.ts new file mode 100644 index 00000000..4260a768 --- /dev/null +++ b/app/src/plugin.hyperlinks.ts @@ -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 + } +} diff --git a/app/src/services/config.ts b/app/src/services/config.ts index 64e0fdaf..360f209b 100644 --- a/app/src/services/config.ts +++ b/app/src/services/config.ts @@ -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' -const Config = nodeRequire('electron-config') +import { ElectronService } from 'services/electron' +const defaultConfig : IConfigData = require('../../defaultConfig.yaml') + +export interface IConfigData { + hotkeys?: any +} @Injectable() export class ConfigService { - constructor() { - this.config = new Config({name: 'config'}) + constructor ( + electron: ElectronService + ) { + this.path = path.join(electron.app.getPath('userData'), 'config.yaml') this.load() } - private config: any - private store: any + private path: string + private store: IConfigData - set(key: string, value: any) { - this.store.set(key, value) - this.save() + load () { + if (fs.existsSync(this.path)) { + this.store = yaml.safeLoad(fs.readFileSync(this.path, 'utf8')) + } else { + this.store = {} + } } - get(key: string): any { - return this.store[key] + save () { + fs.writeFileSync(this.path, yaml.safeDump(this.store), 'utf8') } - has(key: string): boolean { - return this.store[key] != undefined - } - - delete(key: string) { - delete this.store[key] - this.save() - } - - load() { - this.store = this.config.store - } - - save() { - this.config.store = this.store + full () : IConfigData { + return Object.assign({}, defaultConfig, this.store) } } diff --git a/app/src/services/hotkeys.ts b/app/src/services/hotkeys.ts index 94da9ec3..9990b1ae 100644 --- a/app/src/services/hotkeys.ts +++ b/app/src/services/hotkeys.ts @@ -7,7 +7,6 @@ const hterm = require('hterm-commonjs') export interface HotkeyDescription { id: string, name: string, - defaults: string[][], } export interface PartialHotkeyMatch { @@ -21,27 +20,62 @@ 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']], }, { id: 'toggle-last-tab', name: 'Toggle last tab', - defaults: [['Ctrl+A', 'A'], ['Ctrl+A', 'Ctrl+A']], }, { id: 'next-tab', name: 'Next tab', - defaults: [['Ctrl+Shift-ArrowRight'], ['Ctrl+A', 'N']], }, { id: '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) } }) - - if (!config.get('hotkeys')) { - config.set('hotkeys', {}) - } } emitNativeEvent (name, nativeEvent) { @@ -122,18 +152,21 @@ export class HotkeysService { registerHotkeys () { this.electron.globalShortcut.unregisterAll() - this.electron.globalShortcut.register('`', () => { + // TODO + this.electron.globalShortcut.register('PrintScreen', () => { 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] + for (let key in this.config.full().hotkeys) { + let value = this.config.full().hotkeys[key] + if (typeof value == 'string') { + value = [value] + } + value = value.map((item) => (typeof item == 'string') ? [item] : item) + keys[key] = value } return keys } diff --git a/app/src/services/hotkeys.util.ts b/app/src/services/hotkeys.util.ts index 9ec1cf18..2c958d36 100644 --- a/app/src/services/hotkeys.util.ts +++ b/app/src/services/hotkeys.util.ts @@ -52,7 +52,7 @@ export function stringifyKeySequence(events: NativeKeyEvent[]): string[] { continue } itemKeys.push(lastEvent.key) - items.push(itemKeys.join('+')) + items.push(itemKeys.join('-')) } lastEvent = event } diff --git a/app/src/services/pluginDispatcher.ts b/app/src/services/pluginDispatcher.ts new file mode 100644 index 00000000..23f9a71d --- /dev/null +++ b/app/src/services/pluginDispatcher.ts @@ -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) + } + }) + } +} diff --git a/app/src/variables.less b/app/src/variables.less new file mode 100644 index 00000000..3b0add3d --- /dev/null +++ b/app/src/variables.less @@ -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; diff --git a/npm-debug.log b/npm-debug.log new file mode 100644 index 00000000..c3efd975 --- /dev/null +++ b/npm-debug.log @@ -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. (/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. (/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. (/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. (/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. (/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. (/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. (/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. (/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. (/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 ] diff --git a/package.json b/package.json index 6e83b31c..6d1e3098 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "name": "term", "devDependencies": { "apply-loader": "^0.1.0", - "awesome-typescript-loader": "2.2.4", + "awesome-typescript-loader": "3.0.8", "css-loader": "0.26.1", + "dataurl": "^0.1.0", "electron": "^1.4.13", "electron-builder": "10.6.1", "electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04", @@ -11,6 +12,7 @@ "file-loader": "^0.9.0", "font-awesome": "4.7.0", "html-loader": "^0.4.4", + "json-loader": "^0.5.4", "less": "^2.7.1", "less-loader": "^2.2.3", "node-gyp": "^3.4.0", @@ -21,11 +23,11 @@ "style-loader": "^0.13.1", "to-string-loader": "^1.1.5", "tslint": "4.2.0", - "typescript": "2.1.1", - "typings": "2.0.0", + "typescript": "2.2.1", "url-loader": "^0.5.7", "val-loader": "^0.5.0", - "webpack": "2.2.0-rc.4" + "webpack": "2.2.0-rc.4", + "yaml-loader": "^0.4.0" }, "build": { "appId": "com.elements.benchmark", @@ -62,15 +64,20 @@ "@angular/platform-server": "2.3.1", "@angular/router": "3.3.1", "@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-perfect-scrollbar": "^1.1.0", "angular2-toaster": "^1.1.0", "bootstrap": "^3.3.7", "core-js": "^2.4.1", + "hterm-commonjs": "^1.0.0", "jquery": "^3.1.1", "rxjs": "5.0.0-rc.4", "source-sans-pro": "^2.0.10", - "hterm-commonjs": "^1.0.0", "zone.js": "0.7.2" } } diff --git a/typings.json b/typings.json deleted file mode 100644 index d2417300..00000000 --- a/typings.json +++ /dev/null @@ -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" - } -} diff --git a/webpack.config.js b/webpack.config.js index c51b9147..e61ea970 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,6 +64,7 @@ module.exports = { name: 'fonts/[name].[hash:8].[ext]' } }, + { test: /\.yaml$/, loader: "json-loader!yaml-loader" }, ] }, externals: {