This commit is contained in:
Eugene Pankov 2017-04-06 20:06:54 +02:00
parent 7a4806bcc9
commit 0abe1fbc9d
15 changed files with 218 additions and 73 deletions

View File

@ -5,6 +5,7 @@ export { ConfigProvider } from './configProvider'
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider' export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
export { AppService } from 'services/app' export { AppService } from 'services/app'
export { ConfigService } from 'services/config'
export { PluginsService } from 'services/plugins' export { PluginsService } from 'services/plugins'
export { ElectronService } from 'services/electron' export { ElectronService } from 'services/electron'
export { HotkeysService } from 'services/hotkeys' export { HotkeysService } from 'services/hotkeys'

View File

@ -49,7 +49,6 @@
height: @tabs-height; height: @tabs-height;
background: @body-bg; background: @body-bg;
display: flex; display: flex;
flex-direction: row;
&>button { &>button {
line-height: @tabs-height - 2px; line-height: @tabs-height - 2px;
@ -63,7 +62,7 @@
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
color: #888; color: #aaa;
border: none; border: none;
border-radius: 0; border-radius: 0;
@ -72,12 +71,9 @@
} }
} }
&.active-tab-0 .btn-new-tab { &>.tabs-container {
border-bottom-right-radius: @tab-border-radius; flex: auto;
} display: flex;
tab-header.active + button {
border-bottom-left-radius: @tab-border-radius;
} }
} }

View File

@ -3,27 +3,30 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear
.content( .content(
[class.tabs-on-top]='config.full().appearance.tabsOnTop' [class.tabs-on-top]='config.full().appearance.tabsOnTop'
) )
.tabs( .tabs
[class.active-tab-0]='app.tabs[0] == app.activeTab',
)
button.btn.btn-secondary( button.btn.btn-secondary(
*ngFor='let button of getToolbarButtons(false)', *ngFor='let button of getLeftToolbarButtons()',
[title]='button.title', [title]='button.title',
(click)='button.click()', (click)='button.click()',
) )
i.fa([class]='"fa fa-" + button.icon') i.fa([class]='"fa fa-" + button.icon')
tab-header(
*ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', .tabs-container
[index]='idx', tab-header(
[model]='tab', *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id',
[active]='tab == app.activeTab', [class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1',
[hasActivity]='tab.hasActivity', [class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1',
@animateTab, [index]='idx',
(click)='app.selectTab(tab)', [model]='tab',
(closeClicked)='app.closeTab(tab)', [active]='tab == app.activeTab',
) [hasActivity]='tab.hasActivity',
@animateTab,
(click)='app.selectTab(tab)',
(closeClicked)='app.closeTab(tab)',
)
button.btn.btn-secondary( button.btn.btn-secondary(
*ngFor='let button of getToolbarButtons(true)', *ngFor='let button of getRightToolbarButtons()',
[title]='button.title', [title]='button.title',
(click)='button.click()', (click)='button.click()',
) )

View File

@ -127,15 +127,9 @@ export class AppRootComponent {
this.docking.dock() this.docking.dock()
} }
getToolbarButtons (aboveZero: boolean): IToolbarButton[] { getLeftToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(false); }
let buttons: IToolbarButton[] = []
this.toolbarButtonProviders.forEach((provider) => { getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true); }
buttons = buttons.concat(provider.provide())
})
return buttons
.filter((button) => (button.weight > 0) === aboveZero)
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
}
ngOnInit () { 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))
}
} }

View File

@ -26,3 +26,14 @@ export abstract class SessionPersistenceProvider {
abstract async startSession (options: SessionOptions): Promise<any> abstract async startSession (options: SessionOptions): Promise<any>
abstract async terminateSession (recoveryId: string): Promise<void> abstract async terminateSession (recoveryId: string): Promise<void>
} }
export interface ITerminalColorScheme {
name: string
foreground: string
background: string
colors: string[]
}
export abstract class TerminalColorSchemeProvider {
abstract async getSchemes (): Promise<ITerminalColorScheme[]>
}

View File

@ -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<ITerminalColorScheme[]> {
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 = (<any>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
}
}

View File

@ -5,9 +5,46 @@
.appearance-preview( .appearance-preview(
[style.font-family]='config.full().terminal.font', [style.font-family]='config.full().terminal.font',
[style.font-size]='config.full().terminal.fontSize + "px"', [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 div
.text foo bar 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 .col-lg-6
.form-group .form-group
label Font label Font
@ -27,6 +64,15 @@
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
small.form-text.text-muted Text size to be used in the terminal 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 .form-group
label Terminal bell label Terminal bell

View File

@ -1,9 +1,7 @@
.appearance-preview { .appearance-preview {
background: black;
padding: 10px 20px; padding: 10px 20px;
margin: 0 0 10px; margin: 0 0 10px;
span {
.text { white-space: pre;
color: white;
} }
} }

View File

@ -2,9 +2,11 @@ import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import 'rxjs/add/operator/debounceTime' import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/distinctUntilChanged' import 'rxjs/add/operator/distinctUntilChanged'
import { Component } from '@angular/core' const childProcessPromise = require('child-process-promise')
const childProcessPromise = nodeRequire('child-process-promise') const equal = require('deep-equal')
import { Component, Inject } from '@angular/core'
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
@ -14,12 +16,15 @@ import { ConfigService } from 'services/config'
}) })
export class SettingsComponent { export class SettingsComponent {
fonts: string[] = [] fonts: string[] = []
colorSchemes: ITerminalColorScheme[] = []
equalComparator = equal
constructor( constructor(
public config: ConfigService, public config: ConfigService,
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
) { } ) { }
ngOnInit () { async ngOnInit () {
childProcessPromise.exec('fc-list :spacing=mono').then((result) => { childProcessPromise.exec('fc-list :spacing=mono').then((result) => {
this.fonts = result.stdout this.fonts = result.stdout
.split('\n') .split('\n')
@ -28,6 +33,8 @@ export class SettingsComponent {
.map((x) => x.split(',')[0].trim()) .map((x) => x.split(',')[0].trim())
this.fonts.sort() this.fonts.sort()
}) })
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
} }
fontAutocomplete = (text$: Observable<string>) => { fontAutocomplete = (text$: Observable<string>) => {

View File

@ -1,12 +1,18 @@
:host { :host {
flex: auto; flex: auto;
position: relative; display: flex;
display: block;
overflow: hidden; overflow: hidden;
margin: 15px;
&> .content {
flex: auto;
position: relative;
display: block;
overflow: hidden;
margin: 15px;
div[style]:last-child { div[style]:last-child {
background: black !important; background: black !important;
color: white !important; color: white !important;
}
} }
} }

View File

@ -1,18 +1,17 @@
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs' import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs'
import { Component, NgZone, Inject, ElementRef } from '@angular/core' import { Component, NgZone, Inject, ViewChild, HostBinding } from '@angular/core'
import { ConfigService } from 'services/config'
import { BaseTabComponent } from 'components/baseTab' import { BaseTabComponent } from 'components/baseTab'
import { TerminalTab } from '../tab' import { TerminalTab } from '../tab'
import { TerminalDecorator, ResizeEvent } from '../api' import { TerminalDecorator, ResizeEvent } from '../api'
import { AppService, ConfigService } from 'api'
import { hterm, preferenceManager } from '../hterm' import { hterm, preferenceManager } from '../hterm'
@Component({ @Component({
selector: 'terminalTab', selector: 'terminalTab',
template: '', template: '<div #content class="content"></div>',
styles: [require('./terminalTab.scss')], styles: [require('./terminalTab.scss')],
}) })
export class TerminalTabComponent extends BaseTabComponent<TerminalTab> { export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
@ -26,11 +25,13 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
contentUpdated$ = new Subject<void>() contentUpdated$ = new Subject<void>()
alternateScreenActive$ = new BehaviorSubject(false) alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject<Event>() mouseEvent$ = new Subject<Event>()
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
private io: any private io: any
constructor( constructor(
private zone: NgZone, private zone: NgZone,
private elementRef: ElementRef, private app: AppService,
public config: ConfigService, public config: ConfigService,
@Inject(TerminalDecorator) private decorators: TerminalDecorator[], @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
) { ) {
@ -56,20 +57,19 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.hterm.installKeyboard() this.hterm.installKeyboard()
this.io = this.hterm.io.push() this.io = this.hterm.io.push()
this.attachIOHandlers(this.io) this.attachIOHandlers(this.io)
const dataSubscription = this.model.session.dataAvailable.subscribe((data) => { this.model.session.output$.subscribe((data) => {
this.zone.run(() => { this.zone.run(() => {
this.output$.next(data) this.output$.next(data)
}) })
this.write(data) this.write(data)
}) })
const closedSubscription = this.model.session.closed.subscribe(() => { this.model.session.closed$.first().subscribe(() => {
dataSubscription.unsubscribe() this.app.closeTab(this.model)
closedSubscription.unsubscribe()
}) })
this.model.session.releaseInitialDataBuffer() this.model.session.releaseInitialDataBuffer()
} }
this.hterm.decorate(this.elementRef.nativeElement) this.hterm.decorate(this.content.nativeElement)
this.configure() this.configure()
setTimeout(() => { setTimeout(() => {
@ -156,7 +156,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.io.writeUTF8(data) this.io.writeUTF8(data)
} }
configure () { async configure (): Promise<void> {
let config = this.config.full() let config = this.config.full()
preferenceManager.set('font-family', config.terminal.font) preferenceManager.set('font-family', config.terminal.font)
preferenceManager.set('font-size', config.terminal.fontSize) preferenceManager.set('font-size', config.terminal.fontSize)
@ -165,6 +165,18 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
preferenceManager.set('enable-clipboard-notice', false) preferenceManager.set('enable-clipboard-notice', false)
preferenceManager.set('receive-encoding', 'raw') preferenceManager.set('receive-encoding', 'raw')
preferenceManager.set('send-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) this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
} }

View File

@ -8,6 +8,11 @@ export class TerminalConfigProvider extends ConfigProvider {
fontSize: 14, fontSize: 14,
bell: 'off', bell: 'off',
bracketedPaste: true, bracketedPaste: true,
colorScheme: {
foreground: null,
background: null,
colors: null,
},
}, },
hotkeys: { hotkeys: {
'new-tab': [ 'new-tab': [
@ -19,7 +24,9 @@ export class TerminalConfigProvider extends ConfigProvider {
} }
configStructure: any = { configStructure: any = {
terminal: {}, terminal: {
colorScheme: {},
},
hotkeys: {}, hotkeys: {},
} }
} }

View File

@ -13,9 +13,10 @@ import { SessionsService } from './services/sessions'
import { ScreenPersistenceProvider } from './persistenceProviders' import { ScreenPersistenceProvider } from './persistenceProviders'
import { ButtonProvider } from './buttonProvider' import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider' import { RecoveryProvider } from './recoveryProvider'
import { SessionPersistenceProvider } from './api' import { SessionPersistenceProvider, TerminalColorSchemeProvider } from './api'
import { TerminalSettingsProvider } from './settings' import { TerminalSettingsProvider } from './settings'
import { TerminalConfigProvider } from './config' import { TerminalConfigProvider } from './config'
import { HyperColorSchemes } from './colorSchemes'
import { hterm } from './hterm' import { hterm } from './hterm'
@ -26,13 +27,14 @@ import { hterm } from './hterm'
NgbModule, NgbModule,
], ],
providers: [ providers: [
SessionsService,
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
SessionsService,
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider }, { provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider },
// { provide: SessionPersistenceProvider, useValue: null }, // { provide: SessionPersistenceProvider, useValue: null },
{ provide: SettingsTabProvider, useClass: TerminalSettingsProvider, multi: true }, { provide: SettingsTabProvider, useClass: TerminalSettingsProvider, multi: true },
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true }, { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }
], ],
entryComponents: [ entryComponents: [
TerminalTabComponent, TerminalTabComponent,

View File

@ -1,7 +1,8 @@
import * as nodePTY from 'node-pty' import * as nodePTY from 'node-pty'
import * as fs from 'fs-promise' 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 { Logger, LogService } from 'services/log'
import { SessionOptions, SessionPersistenceProvider } from '../api' import { SessionOptions, SessionPersistenceProvider } from '../api'
@ -9,9 +10,9 @@ import { SessionOptions, SessionPersistenceProvider } from '../api'
export class Session { export class Session {
open: boolean open: boolean
name: string name: string
dataAvailable = new EventEmitter() output$ = new Subject<string>()
closed = new EventEmitter() closed$ = new Subject<void>()
destroyed = new EventEmitter() destroyed$ = new Subject<void>()
recoveryId: string recoveryId: string
truePID: number truePID: number
private pty: any private pty: any
@ -50,19 +51,18 @@ export class Session {
if (!this.initialDataBufferReleased) { if (!this.initialDataBufferReleased) {
this.initialDataBuffer += data this.initialDataBuffer += data
} else { } else {
this.dataAvailable.emit(data) this.output$.next(data)
} }
}) })
this.pty.on('close', () => { this.pty.on('close', () => {
this.open = false this.close()
this.closed.emit()
}) })
} }
releaseInitialDataBuffer () { releaseInitialDataBuffer () {
this.initialDataBufferReleased = true this.initialDataBufferReleased = true
this.dataAvailable.emit(this.initialDataBuffer) this.output$.next(this.initialDataBuffer)
this.initialDataBuffer = null this.initialDataBuffer = null
} }
@ -80,7 +80,7 @@ export class Session {
close () { close () {
this.open = false this.open = false
this.closed.emit() this.closed$.next()
this.pty.end() this.pty.end()
} }
@ -106,8 +106,9 @@ export class Session {
if (open) { if (open) {
this.close() this.close()
} }
this.destroyed.emit() this.destroyed$.next()
this.pty.destroy() this.pty.destroy()
this.output$.complete()
} }
async getWorkingDirectory (): Promise<string> { async getWorkingDirectory (): Promise<string> {
@ -142,12 +143,11 @@ export class SessionsService {
this.lastID++ this.lastID++
options.name = `session-${this.lastID}` options.name = `session-${this.lastID}`
let session = new Session(options) let session = new Session(options)
const destroySubscription = session.destroyed.subscribe(() => { session.destroyed$.first().subscribe(() => {
delete this.sessions[session.name] delete this.sessions[session.name]
if (this.persistence) { if (this.persistence) {
this.persistence.terminateSession(session.recoveryId) this.persistence.terminateSession(session.recoveryId)
} }
destroySubscription.unsubscribe()
}) })
this.sessions[session.name] = session this.sessions[session.name] = session
return session return session

View File

@ -7,6 +7,7 @@
"awesome-typescript-loader": "3.0.8", "awesome-typescript-loader": "3.0.8",
"css-loader": "0.26.1", "css-loader": "0.26.1",
"dataurl": "^0.1.0", "dataurl": "^0.1.0",
"deep-equal": "^1.0.1",
"electron": "1.6.2", "electron": "1.6.2",
"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",