diff --git a/app/src/api/index.ts b/app/src/api/index.ts index d5910cd8..bdc4544c 100644 --- a/app/src/api/index.ts +++ b/app/src/api/index.ts @@ -5,6 +5,7 @@ export { ConfigProvider } from './configProvider' export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider' export { AppService } from 'services/app' +export { ConfigService } from 'services/config' export { PluginsService } from 'services/plugins' export { ElectronService } from 'services/electron' export { HotkeysService } from 'services/hotkeys' diff --git a/app/src/components/appRoot.less b/app/src/components/appRoot.less index 5ba2e7e8..0cbcce99 100644 --- a/app/src/components/appRoot.less +++ b/app/src/components/appRoot.less @@ -49,7 +49,6 @@ height: @tabs-height; background: @body-bg; display: flex; - flex-direction: row; &>button { line-height: @tabs-height - 2px; @@ -63,7 +62,7 @@ text-transform: uppercase; font-weight: bold; - color: #888; + color: #aaa; border: none; border-radius: 0; @@ -72,12 +71,9 @@ } } - &.active-tab-0 .btn-new-tab { - border-bottom-right-radius: @tab-border-radius; - } - - tab-header.active + button { - border-bottom-left-radius: @tab-border-radius; + &>.tabs-container { + flex: auto; + display: flex; } } diff --git a/app/src/components/appRoot.pug b/app/src/components/appRoot.pug index 36e47872..34a10f29 100644 --- a/app/src/components/appRoot.pug +++ b/app/src/components/appRoot.pug @@ -3,27 +3,30 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear .content( [class.tabs-on-top]='config.full().appearance.tabsOnTop' ) - .tabs( - [class.active-tab-0]='app.tabs[0] == app.activeTab', - ) + .tabs button.btn.btn-secondary( - *ngFor='let button of getToolbarButtons(false)', + *ngFor='let button of getLeftToolbarButtons()', [title]='button.title', (click)='button.click()', ) i.fa([class]='"fa fa-" + button.icon') - tab-header( - *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', - [index]='idx', - [model]='tab', - [active]='tab == app.activeTab', - [hasActivity]='tab.hasActivity', - @animateTab, - (click)='app.selectTab(tab)', - (closeClicked)='app.closeTab(tab)', - ) + + .tabs-container + tab-header( + *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', + [class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1', + [class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1', + [index]='idx', + [model]='tab', + [active]='tab == app.activeTab', + [hasActivity]='tab.hasActivity', + @animateTab, + (click)='app.selectTab(tab)', + (closeClicked)='app.closeTab(tab)', + ) + button.btn.btn-secondary( - *ngFor='let button of getToolbarButtons(true)', + *ngFor='let button of getRightToolbarButtons()', [title]='button.title', (click)='button.click()', ) diff --git a/app/src/components/appRoot.ts b/app/src/components/appRoot.ts index 4d5fc6aa..b8915c2f 100644 --- a/app/src/components/appRoot.ts +++ b/app/src/components/appRoot.ts @@ -127,15 +127,9 @@ export class AppRootComponent { this.docking.dock() } - getToolbarButtons (aboveZero: boolean): IToolbarButton[] { - let buttons: IToolbarButton[] = [] - this.toolbarButtonProviders.forEach((provider) => { - buttons = buttons.concat(provider.provide()) - }) - return buttons - .filter((button) => (button.weight > 0) === aboveZero) - .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) - } + getLeftToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(false); } + + getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true); } ngOnInit () { /* @@ -151,4 +145,15 @@ export class AppRootComponent { }) */ } + + private getToolbarButtons (aboveZero: boolean): IToolbarButton[] { + let buttons: IToolbarButton[] = [] + this.toolbarButtonProviders.forEach((provider) => { + buttons = buttons.concat(provider.provide()) + }) + return buttons + .filter((button) => (button.weight > 0) === aboveZero) + .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) + } + } diff --git a/app/src/terminal/api.ts b/app/src/terminal/api.ts index 212ad698..fbe16e7c 100644 --- a/app/src/terminal/api.ts +++ b/app/src/terminal/api.ts @@ -26,3 +26,14 @@ export abstract class SessionPersistenceProvider { abstract async startSession (options: SessionOptions): Promise abstract async terminateSession (recoveryId: string): Promise } + +export interface ITerminalColorScheme { + name: string + foreground: string + background: string + colors: string[] +} + +export abstract class TerminalColorSchemeProvider { + abstract async getSchemes (): Promise +} diff --git a/app/src/terminal/colorSchemes.ts b/app/src/terminal/colorSchemes.ts new file mode 100644 index 00000000..27721480 --- /dev/null +++ b/app/src/terminal/colorSchemes.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs-promise' +import * as path from 'path' +import { Injectable } from '@angular/core' +import { TerminalColorSchemeProvider, ITerminalColorScheme } from './api' + + +@Injectable() +export class HyperColorSchemes extends TerminalColorSchemeProvider { + async getSchemes (): Promise { + let pluginsPath = path.join(process.env.HOME, '.hyper_plugins', 'node_modules') + if (!(await fs.exists(pluginsPath))) return [] + let plugins = await fs.readdir(pluginsPath) + + let themes: ITerminalColorScheme[] = [] + + plugins.forEach(plugin => { + let module = (global).require(path.join(pluginsPath, plugin)) + if (module.decorateConfig) { + let config = module.decorateConfig({}) + if (config.colors) { + themes.push({ + name: plugin, + foreground: config.foregroundColor, + background: config.backgroundColor, + colors: config.colors.black ? [ + config.colors.black, + config.colors.red, + config.colors.green, + config.colors.yellow, + config.colors.blue, + config.colors.magenta, + config.colors.cyan, + config.colors.white, + config.colors.lightBlack, + config.colors.lightRed, + config.colors.lightGreen, + config.colors.lightYellow, + config.colors.lightBlue, + config.colors.lightMagenta, + config.colors.lightCyan, + config.colors.lightWhite, + ] : config.colors, + }) + } + } + }) + + return themes + } +} diff --git a/app/src/terminal/components/settings.pug b/app/src/terminal/components/settings.pug index a424209a..50319977 100644 --- a/app/src/terminal/components/settings.pug +++ b/app/src/terminal/components/settings.pug @@ -5,9 +5,46 @@ .appearance-preview( [style.font-family]='config.full().terminal.font', [style.font-size]='config.full().terminal.fontSize + "px"', + [style.background-color]='config.full().terminal.colorScheme.background', + [style.color]='config.full().terminal.colorScheme.foreground', ) - .text john@doe-pc$ ls - .text foo bar + div + span john@doe-pc + span([style.color]='config.full().terminal.colorScheme.colors[1]') $ + span webpack + div + span Asset Size + div + span([style.color]='config.full().terminal.colorScheme.colors[2]') main.js + span 234 kB + span([style.color]='config.full().terminal.colorScheme.colors[2]') [emitted] + div + span([style.color]='config.full().terminal.colorScheme.colors[3]') big.js + span([style.color]='config.full().terminal.colorScheme.colors[3]') 1.2 MB + span([style.color]='config.full().terminal.colorScheme.colors[2]') [emitted] + span([style.color]='config.full().terminal.colorScheme.colors[3]') [big] + div + span + div + span john@doe-pc + span([style.color]='config.full().terminal.colorScheme.colors[1]') $ + span ls -l + div + span drwxr-xr-x 1 root root + span([style.color]='config.full().terminal.colorScheme.colors[4]') directory + div + span -rw-r--r-- 1 root root file + div + span -rwxr-xr-x 1 root root + span([style.color]='config.full().terminal.colorScheme.colors[2]') executable + div + span -rwxr-xr-x 1 root root + span([style.color]='config.full().terminal.colorScheme.colors[6]') sym + span -> + span([style.color]='config.full().terminal.colorScheme.colors[1]') link + div + + .col-lg-6 .form-group label Font @@ -27,6 +64,15 @@ (ngModelChange)='config.save()', ) small.form-text.text-muted Text size to be used in the terminal + + .form-group + label Color scheme + select.form-control( + [compareWith]='equalComparator', + '[(ngModel)]'='config.store.terminal.colorScheme', + (ngModelChange)='config.save()', + ) + option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}} .form-group label Terminal bell diff --git a/app/src/terminal/components/settings.scss b/app/src/terminal/components/settings.scss index c085ce3a..8af3f5f5 100644 --- a/app/src/terminal/components/settings.scss +++ b/app/src/terminal/components/settings.scss @@ -1,9 +1,7 @@ .appearance-preview { - background: black; padding: 10px 20px; margin: 0 0 10px; - - .text { - color: white; + span { + white-space: pre; } } diff --git a/app/src/terminal/components/settings.ts b/app/src/terminal/components/settings.ts index d6ce6d34..73a83905 100644 --- a/app/src/terminal/components/settings.ts +++ b/app/src/terminal/components/settings.ts @@ -2,9 +2,11 @@ import { Observable } from 'rxjs/Observable' import 'rxjs/add/operator/map' import 'rxjs/add/operator/debounceTime' import 'rxjs/add/operator/distinctUntilChanged' -import { Component } from '@angular/core' -const childProcessPromise = nodeRequire('child-process-promise') +const childProcessPromise = require('child-process-promise') +const equal = require('deep-equal') +import { Component, Inject } from '@angular/core' +import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api' import { ConfigService } from 'services/config' @@ -14,12 +16,15 @@ import { ConfigService } from 'services/config' }) export class SettingsComponent { fonts: string[] = [] + colorSchemes: ITerminalColorScheme[] = [] + equalComparator = equal constructor( public config: ConfigService, + @Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[], ) { } - ngOnInit () { + async ngOnInit () { childProcessPromise.exec('fc-list :spacing=mono').then((result) => { this.fonts = result.stdout .split('\n') @@ -28,6 +33,8 @@ export class SettingsComponent { .map((x) => x.split(',')[0].trim()) this.fonts.sort() }) + + this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b)) } fontAutocomplete = (text$: Observable) => { diff --git a/app/src/terminal/components/terminalTab.scss b/app/src/terminal/components/terminalTab.scss index 95037e8c..f85f0f7c 100644 --- a/app/src/terminal/components/terminalTab.scss +++ b/app/src/terminal/components/terminalTab.scss @@ -1,12 +1,18 @@ :host { flex: auto; - position: relative; - display: block; + display: flex; overflow: hidden; - margin: 15px; + + &> .content { + flex: auto; + position: relative; + display: block; + overflow: hidden; + margin: 15px; - div[style]:last-child { - background: black !important; - color: white !important; + div[style]:last-child { + background: black !important; + color: white !important; + } } } diff --git a/app/src/terminal/components/terminalTab.ts b/app/src/terminal/components/terminalTab.ts index 1a1a9492..e003402d 100644 --- a/app/src/terminal/components/terminalTab.ts +++ b/app/src/terminal/components/terminalTab.ts @@ -1,18 +1,17 @@ import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs' -import { Component, NgZone, Inject, ElementRef } from '@angular/core' - -import { ConfigService } from 'services/config' +import { Component, NgZone, Inject, ViewChild, HostBinding } from '@angular/core' import { BaseTabComponent } from 'components/baseTab' import { TerminalTab } from '../tab' import { TerminalDecorator, ResizeEvent } from '../api' +import { AppService, ConfigService } from 'api' import { hterm, preferenceManager } from '../hterm' @Component({ selector: 'terminalTab', - template: '', + template: '
', styles: [require('./terminalTab.scss')], }) export class TerminalTabComponent extends BaseTabComponent { @@ -26,11 +25,13 @@ export class TerminalTabComponent extends BaseTabComponent { contentUpdated$ = new Subject() alternateScreenActive$ = new BehaviorSubject(false) mouseEvent$ = new Subject() + @ViewChild('content') content + @HostBinding('style.background-color') backgroundColor: string private io: any constructor( private zone: NgZone, - private elementRef: ElementRef, + private app: AppService, public config: ConfigService, @Inject(TerminalDecorator) private decorators: TerminalDecorator[], ) { @@ -56,20 +57,19 @@ export class TerminalTabComponent extends BaseTabComponent { this.hterm.installKeyboard() this.io = this.hterm.io.push() this.attachIOHandlers(this.io) - const dataSubscription = this.model.session.dataAvailable.subscribe((data) => { + this.model.session.output$.subscribe((data) => { this.zone.run(() => { this.output$.next(data) }) this.write(data) }) - const closedSubscription = this.model.session.closed.subscribe(() => { - dataSubscription.unsubscribe() - closedSubscription.unsubscribe() + this.model.session.closed$.first().subscribe(() => { + this.app.closeTab(this.model) }) this.model.session.releaseInitialDataBuffer() } - this.hterm.decorate(this.elementRef.nativeElement) + this.hterm.decorate(this.content.nativeElement) this.configure() setTimeout(() => { @@ -156,7 +156,7 @@ export class TerminalTabComponent extends BaseTabComponent { this.io.writeUTF8(data) } - configure () { + async configure (): Promise { let config = this.config.full() preferenceManager.set('font-family', config.terminal.font) preferenceManager.set('font-size', config.terminal.fontSize) @@ -165,6 +165,18 @@ export class TerminalTabComponent extends BaseTabComponent { preferenceManager.set('enable-clipboard-notice', false) preferenceManager.set('receive-encoding', 'raw') preferenceManager.set('send-encoding', 'raw') + + if (config.terminal.colorScheme.foreground) { + preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground) + } + if (config.terminal.colorScheme.background) { + preferenceManager.set('background-color', config.terminal.colorScheme.background) + this.backgroundColor = config.terminal.colorScheme.background + } + if (config.terminal.colorScheme.colors) { + preferenceManager.set('color-palette-overrides', config.terminal.colorScheme.colors) + } + this.hterm.setBracketedPaste(config.terminal.bracketedPaste) } diff --git a/app/src/terminal/config.ts b/app/src/terminal/config.ts index 8cafae23..901df0e8 100644 --- a/app/src/terminal/config.ts +++ b/app/src/terminal/config.ts @@ -8,6 +8,11 @@ export class TerminalConfigProvider extends ConfigProvider { fontSize: 14, bell: 'off', bracketedPaste: true, + colorScheme: { + foreground: null, + background: null, + colors: null, + }, }, hotkeys: { 'new-tab': [ @@ -19,7 +24,9 @@ export class TerminalConfigProvider extends ConfigProvider { } configStructure: any = { - terminal: {}, + terminal: { + colorScheme: {}, + }, hotkeys: {}, } } diff --git a/app/src/terminal/index.ts b/app/src/terminal/index.ts index a26e9600..323a780f 100644 --- a/app/src/terminal/index.ts +++ b/app/src/terminal/index.ts @@ -13,9 +13,10 @@ import { SessionsService } from './services/sessions' import { ScreenPersistenceProvider } from './persistenceProviders' import { ButtonProvider } from './buttonProvider' import { RecoveryProvider } from './recoveryProvider' -import { SessionPersistenceProvider } from './api' +import { SessionPersistenceProvider, TerminalColorSchemeProvider } from './api' import { TerminalSettingsProvider } from './settings' import { TerminalConfigProvider } from './config' +import { HyperColorSchemes } from './colorSchemes' import { hterm } from './hterm' @@ -26,13 +27,14 @@ import { hterm } from './hterm' NgbModule, ], providers: [ + SessionsService, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, - SessionsService, { provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider }, // { provide: SessionPersistenceProvider, useValue: null }, { provide: SettingsTabProvider, useClass: TerminalSettingsProvider, multi: true }, { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true }, + { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true } ], entryComponents: [ TerminalTabComponent, diff --git a/app/src/terminal/services/sessions.ts b/app/src/terminal/services/sessions.ts index a2c469b6..0a63bd74 100644 --- a/app/src/terminal/services/sessions.ts +++ b/app/src/terminal/services/sessions.ts @@ -1,7 +1,8 @@ import * as nodePTY from 'node-pty' import * as fs from 'fs-promise' -import { Injectable, EventEmitter } from '@angular/core' +import { Subject } from 'rxjs' +import { Injectable } from '@angular/core' import { Logger, LogService } from 'services/log' import { SessionOptions, SessionPersistenceProvider } from '../api' @@ -9,9 +10,9 @@ import { SessionOptions, SessionPersistenceProvider } from '../api' export class Session { open: boolean name: string - dataAvailable = new EventEmitter() - closed = new EventEmitter() - destroyed = new EventEmitter() + output$ = new Subject() + closed$ = new Subject() + destroyed$ = new Subject() recoveryId: string truePID: number private pty: any @@ -50,19 +51,18 @@ export class Session { if (!this.initialDataBufferReleased) { this.initialDataBuffer += data } else { - this.dataAvailable.emit(data) + this.output$.next(data) } }) this.pty.on('close', () => { - this.open = false - this.closed.emit() + this.close() }) } releaseInitialDataBuffer () { this.initialDataBufferReleased = true - this.dataAvailable.emit(this.initialDataBuffer) + this.output$.next(this.initialDataBuffer) this.initialDataBuffer = null } @@ -80,7 +80,7 @@ export class Session { close () { this.open = false - this.closed.emit() + this.closed$.next() this.pty.end() } @@ -106,8 +106,9 @@ export class Session { if (open) { this.close() } - this.destroyed.emit() + this.destroyed$.next() this.pty.destroy() + this.output$.complete() } async getWorkingDirectory (): Promise { @@ -142,12 +143,11 @@ export class SessionsService { this.lastID++ options.name = `session-${this.lastID}` let session = new Session(options) - const destroySubscription = session.destroyed.subscribe(() => { + session.destroyed$.first().subscribe(() => { delete this.sessions[session.name] if (this.persistence) { this.persistence.terminateSession(session.recoveryId) } - destroySubscription.unsubscribe() }) this.sessions[session.name] = session return session diff --git a/package.json b/package.json index 9de1998e..ca6abf21 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "awesome-typescript-loader": "3.0.8", "css-loader": "0.26.1", "dataurl": "^0.1.0", + "deep-equal": "^1.0.1", "electron": "1.6.2", "electron-builder": "10.6.1", "electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04",