This commit is contained in:
Eugene Pankov 2017-04-16 23:04:29 +02:00
parent 125ec2b81c
commit acc13087bf
18 changed files with 91 additions and 33 deletions

View File

@ -6,7 +6,7 @@ SHORT_VERSION=$(shell python -c 'import subprocess; v = subprocess.check_output(
all: run all: run
run: run:
DEV=1 TERMINUS_PLUGINS=`realpath .` ./node_modules/.bin/electron ./app --debug DEV=1 TERMINUS_PLUGINS=$$PWD ./node_modules/.bin/electron ./app --debug
lint: lint:
tslint -c tslint.json app/src/*.ts app/src/**/*.ts tslint -c tslint.json app/src/*.ts app/src/**/*.ts

View File

@ -3,6 +3,7 @@
import 'core-js' import 'core-js'
import 'zone.js/dist/zone.js' import 'zone.js/dist/zone.js'
import 'core-js/es7/reflect' import 'core-js/es7/reflect'
import 'rxjs'
// Always land on the start view // Always land on the start view
location.hash = '' location.hash = ''

View File

@ -5,7 +5,7 @@ let nodeRequire = (<any>global).require
let module = nodeRequire('module') let module = nodeRequire('module')
nodeRequire.main.paths.map(x => module.globalPaths.push(x)) nodeRequire.main.paths.map(x => module.globalPaths.push(x))
if (process.env.TERMINUS_PLUGINS) { if (process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS.split(':').map(x => module.globalPaths.push(x)) process.env.TERMINUS_PLUGINS.split(':').map(x => module.globalPaths.unshift(x))
} }
export async function loadPlugins (): Promise<any[]> { export async function loadPlugins (): Promise<any[]> {

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "src", "baseUrl": "src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,

View File

@ -21,7 +21,7 @@ export class ColorSchemes extends TerminalColorSchemeProvider {
}) })
schemes.push({ schemes.push({
name: schemeFile.split('/')[1], name: schemeFile.split('/')[1].trim(),
foreground: values.foreground, foreground: values.foreground,
background: values.background, background: values.background,
cursor: values.cursorColor, cursor: values.cursorColor,

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "src", "baseUrl": "src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,

View File

@ -40,7 +40,7 @@ import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
}) })
export class AppRootComponent { export class AppRootComponent {
toasterConfig: ToasterConfig toasterConfig: ToasterConfig
logger: Logger private logger: Logger
constructor( constructor(
private docking: DockingService, private docking: DockingService,

View File

@ -1,5 +1,5 @@
import { BehaviorSubject } from 'rxjs' import { Subject, BehaviorSubject } from 'rxjs'
import { EventEmitter, ViewRef } from '@angular/core' import { ViewRef } from '@angular/core'
export abstract class BaseTabComponent { export abstract class BaseTabComponent {
@ -7,13 +7,20 @@ export abstract class BaseTabComponent {
title$ = new BehaviorSubject<string>(null) title$ = new BehaviorSubject<string>(null)
scrollable: boolean scrollable: boolean
hasActivity = false hasActivity = false
focused = new EventEmitter<any>() focused$ = new Subject<void>()
blurred = new EventEmitter<any>() blurred$ = new Subject<void>()
hasFocus = false
hostView: ViewRef hostView: ViewRef
private static lastTabID = 0 private static lastTabID = 0
constructor () { constructor () {
this.id = BaseTabComponent.lastTabID++ this.id = BaseTabComponent.lastTabID++
this.focused$.subscribe(() => {
this.hasFocus = true
})
this.blurred$.subscribe(() => {
this.hasFocus = false
})
} }
displayActivity (): void { displayActivity (): void {
@ -25,5 +32,8 @@ export abstract class BaseTabComponent {
} }
destroy (): void { destroy (): void {
this.focused$.complete()
this.blurred$.complete()
this.title$.complete()
} }
} }

View File

@ -4,9 +4,10 @@ import { BaseTabComponent } from '../components/baseTab.component'
@Component({ @Component({
selector: 'tab-body', selector: 'tab-body',
template: ` template: `
<perfect-scrollbar [config]="{ suppressScrollX: true, suppressScrollY: !scrollable}"> <perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
<ng-template #placeholder></ng-template> <ng-template #scrollablePlaceholder></ng-template>
</perfect-scrollbar> </perfect-scrollbar>
<template #nonScrollablePlaceholder [ngIf]="!scrollable"></template>
`, `,
styles: [ styles: [
require('./tabBody.component.scss'), require('./tabBody.component.scss'),
@ -17,11 +18,12 @@ export class TabBodyComponent {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@Input() scrollable: boolean @Input() scrollable: boolean
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('scrollablePlaceholder', {read: ViewContainerRef}) scrollablePlaceholder: ViewContainerRef
@ViewChild('nonScrollablePlaceholder', {read: ViewContainerRef}) nonScrollablePlaceholder: ViewContainerRef
ngAfterViewInit () { ngAfterViewInit () {
setImmediate(() => { setImmediate(() => {
this.placeholder.insert(this.tab.hostView) (this.scrollable ? this.scrollablePlaceholder : this.nonScrollablePlaceholder).insert(this.tab.hostView)
}) })
} }
} }

View File

@ -51,11 +51,11 @@ export class AppService {
} }
if (this.activeTab) { if (this.activeTab) {
this.activeTab.hasActivity = false this.activeTab.hasActivity = false
this.activeTab.blurred.emit() this.activeTab.blurred$.next()
} }
this.activeTab = tab this.activeTab = tab
if (this.activeTab) { if (this.activeTab) {
this.activeTab.focused.emit() this.activeTab.focused$.next()
} }
} }

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src", "baseUrl": "./src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src", "baseUrl": "./src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,

View File

@ -1,7 +1,5 @@
import { Observable } from 'rxjs/Observable' import 'rxjs'
import 'rxjs/add/operator/map' import { Observable } from 'rxjs'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/distinctUntilChanged'
import * as fs from 'fs-promise' import * as fs from 'fs-promise'
const fontManager = require('font-manager') const fontManager = require('font-manager')
const equal = require('deep-equal') const equal = require('deep-equal')
@ -45,7 +43,8 @@ export class TerminalSettingsTabComponent {
.map(x => x.split(',')[0].trim()) .map(x => x.split(',')[0].trim())
this.fonts.sort() this.fonts.sort()
}) })
}
if (this.hostApp.platform == Platform.Linux || this.hostApp.platform == Platform.macOS) {
this.shells = (await fs.readFile('/etc/shells', 'utf-8')) this.shells = (await fs.readFile('/etc/shells', 'utf-8'))
.split('\n') .split('\n')
.map(x => x.trim()) .map(x => x.trim())

View File

@ -16,7 +16,6 @@ import { hterm, preferenceManager } from '../hterm'
export class TerminalTabComponent extends BaseTabComponent { export class TerminalTabComponent extends BaseTabComponent {
hterm: any hterm: any
configSubscription: Subscription configSubscription: Subscription
focusedSubscription: Subscription
bell$ = new Subject() bell$ = new Subject()
size$ = new ReplaySubject<ResizeEvent>(1) size$ = new ReplaySubject<ResizeEvent>(1)
input$ = new Subject<string>() input$ = new Subject<string>()
@ -49,8 +48,9 @@ export class TerminalTabComponent extends BaseTabComponent {
} }
ngOnInit () { ngOnInit () {
this.focusedSubscription = this.focused.subscribe(() => { this.focused$.subscribe(() => {
this.hterm.scrollPort_.focus() this.hterm.scrollPort_.focus()
this.hterm.scrollPort_.resize()
}) })
this.hterm = new hterm.hterm.Terminal() this.hterm = new hterm.hterm.Terminal()
@ -123,6 +123,14 @@ export class TerminalTabComponent extends BaseTabComponent {
event.preventDefault() event.preventDefault()
} }
const _resize = hterm.scrollPort_.resize.bind(hterm.scrollPort_)
hterm.scrollPort_.resize = () => {
if (!this.hasFocus) {
return
}
_resize()
}
const _onMouse_ = hterm.onMouse_.bind(hterm) const _onMouse_ = hterm.onMouse_.bind(hterm)
hterm.onMouse_ = (event) => { hterm.onMouse_ = (event) => {
this.mouseEvent$.next(event) this.mouseEvent$.next(event)
@ -216,9 +224,7 @@ export class TerminalTabComponent extends BaseTabComponent {
this.decorators.forEach((decorator) => { this.decorators.forEach((decorator) => {
decorator.detach(this) decorator.detach(this)
}) })
this.focusedSubscription.unsubscribe()
this.configSubscription.unsubscribe() this.configSubscription.unsubscribe()
this.title$.complete()
this.size$.complete() this.size$.complete()
this.input$.complete() this.input$.complete()
this.output$.complete() this.output$.complete()

View File

@ -1,10 +1,40 @@
import * as fs from 'fs-promise' import * as fs from 'fs-promise'
const { exec, spawn } = require('child-process-promise') const { exec, spawn } = require('child-process-promise')
import { Injectable } from '@angular/core'
import { Logger, LogService } from 'terminus-core'
import { SessionOptions, SessionPersistenceProvider } from './api' import { SessionOptions, SessionPersistenceProvider } from './api'
interface IChildProcess {
pid: number
ppid: number
command: string
}
async function listProcesses (): Promise<IChildProcess[]> {
return (await exec(`ps -A -o pid,ppid,command`)).stdout
.split('\n')
.slice(1)
.map(line => line.split(' ').filter(x => x).slice(0, 3))
.map(([pid, ppid, command]) => {
return {
pid: parseInt(pid), ppid: parseInt(ppid), command
}
})
}
@Injectable()
export class ScreenPersistenceProvider extends SessionPersistenceProvider { export class ScreenPersistenceProvider extends SessionPersistenceProvider {
private logger: Logger
constructor (
log: LogService,
) {
super()
this.logger = log.create('main')
}
async attachSession (recoveryId: any): Promise<SessionOptions> { async attachSession (recoveryId: any): Promise<SessionOptions> {
let lines: string[] let lines: string[]
try { try {
@ -20,8 +50,16 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
return null return null
} }
lines = (await exec(`pgrep -P ${screenPID}`)).stdout.split('\n') let child = (await listProcesses()).find(x => x.ppid === screenPID)
let recoveredTruePID = parseInt(lines[0])
if (!child) {
this.logger.error(`Could not find any children of the screen process (PID ${screenPID})!`)
}
if (child.command == 'login') {
child = (await listProcesses()).find(x => x.ppid === child.pid)
}
let recoveredTruePID = child.pid
return { return {
recoveryId, recoveryId,
@ -36,6 +74,7 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
await fs.writeFile(configPath, ` await fs.writeFile(configPath, `
escape ^^^ escape ^^^
vbell on vbell on
deflogin off
term xterm-color term xterm-color
bindkey "^[OH" beginning-of-line bindkey "^[OH" beginning-of-line
bindkey "^[OF" end-of-line bindkey "^[OF" end-of-line
@ -48,6 +87,7 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
`, 'utf-8') `, 'utf-8')
let recoveryId = `term-tab-${Date.now()}` let recoveryId = `term-tab-${Date.now()}`
let args = ['-d', '-m', '-c', configPath, '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', options.command].concat(options.args || []) let args = ['-d', '-m', '-c', configPath, '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', options.command].concat(options.args || [])
this.logger.debug('Spawning screen with', args.join(' '))
await spawn('screen', args, { await spawn('screen', args, {
cwd: options.cwd, cwd: options.cwd,
env: options.env || process.env, env: options.env || process.env,

View File

@ -113,8 +113,8 @@ export class Session {
async getWorkingDirectory (): Promise<string> { async getWorkingDirectory (): Promise<string> {
if (process.platform == 'darwin') { if (process.platform == 'darwin') {
let lines = (await exec(`lsof -p ${this.truePID} -Fn`)).split('\n') let lines = (await exec(`lsof -p ${this.truePID} -Fn`)).stdout.split('\n')
return lines[2] return lines[2].substring(1)
} }
if (process.platform == 'linux') { if (process.platform == 'linux') {
return await fs.readlink(`/proc/${this.truePID}/cwd`) return await fs.readlink(`/proc/${this.truePID}/cwd`)

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "src", "baseUrl": "src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "src", "baseUrl": "src",
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
"removeComments": false, "removeComments": false,