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 { AppService } from 'services/app'
export { ConfigService } from 'services/config'
export { PluginsService } from 'services/plugins'
export { ElectronService } from 'services/electron'
export { HotkeysService } from 'services/hotkeys'

View file

@ -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;
}
}

View file

@ -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()',
)

View file

@ -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))
}
}

View file

@ -26,3 +26,14 @@ export abstract class SessionPersistenceProvider {
abstract async startSession (options: SessionOptions): Promise<any>
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(
[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

View file

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

View file

@ -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<string>) => {

View file

@ -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;
}
}
}

View file

@ -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: '<div #content class="content"></div>',
styles: [require('./terminalTab.scss')],
})
export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
@ -26,11 +25,13 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
contentUpdated$ = new Subject<void>()
alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject<Event>()
@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<TerminalTab> {
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<TerminalTab> {
this.io.writeUTF8(data)
}
configure () {
async configure (): Promise<void> {
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<TerminalTab> {
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)
}

View file

@ -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: {},
}
}

View file

@ -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,

View file

@ -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<string>()
closed$ = new Subject<void>()
destroyed$ = new Subject<void>()
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<string> {
@ -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

View file

@ -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",