mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-20 02:18:01 +00:00
.
This commit is contained in:
@@ -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
47
app/defaultConfig.yaml
Normal 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']
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -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({
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -1,4 +1,6 @@
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
|
||||
>.modal-body {
|
||||
padding: 0 0 20px !important;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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<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() {
|
||||
this.config.save()
|
||||
}
|
||||
|
@@ -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 () {
|
||||
|
@@ -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'
|
||||
|
@@ -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();
|
||||
}
|
||||
|
24
app/src/mixins.less
Normal file
24
app/src/mixins.less
Normal 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();
|
||||
}
|
123
app/src/plugin.hyperlinks.ts
Normal file
123
app/src/plugin.hyperlinks.ts
Normal 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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
32
app/src/services/pluginDispatcher.ts
Normal file
32
app/src/services/pluginDispatcher.ts
Normal 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
16
app/src/variables.less
Normal 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;
|
Reference in New Issue
Block a user