diff --git a/app/assets/img/disk.icns b/app/assets/img/disk.icns deleted file mode 100644 index e25108c2..00000000 Binary files a/app/assets/img/disk.icns and /dev/null differ diff --git a/app/assets/img/disk.ico b/app/assets/img/disk.ico deleted file mode 100644 index a855fae8..00000000 Binary files a/app/assets/img/disk.ico and /dev/null differ diff --git a/app/assets/img/logo.png b/app/assets/img/logo.png deleted file mode 100644 index b94ffe44..00000000 Binary files a/app/assets/img/logo.png and /dev/null differ diff --git a/app/assets/img/logo.svg b/app/assets/img/logo.svg deleted file mode 100644 index 8a04e838..00000000 --- a/app/assets/img/logo.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/app/assets/img/user.png b/app/assets/img/user.png deleted file mode 100644 index 4b29cd32..00000000 Binary files a/app/assets/img/user.png and /dev/null differ diff --git a/app/src/app.module.ts b/app/src/app.module.ts index 3397dfe0..0a6c6662 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -4,13 +4,6 @@ import { HttpModule } from '@angular/http' import { FormsModule } from '@angular/forms' import { ToasterModule } from 'angular2-toaster' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' -import { PerfectScrollbarModule } from 'angular2-perfect-scrollbar' -import { PerfectScrollbarConfigInterface } from 'angular2-perfect-scrollbar' - -const PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { - suppressScrollX: true -} - import { ConfigService } from 'services/config' import { ElectronService } from 'services/electron' @@ -19,11 +12,13 @@ import { LogService } from 'services/log' import { ModalService } from 'services/modal' import { NotifyService } from 'services/notify' import { QuitterService } from 'services/quitter' +import { SessionsService } from 'services/sessions' import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' import { AppComponent } from 'components/app' import { CheckboxComponent } from 'components/checkbox' import { SettingsModalComponent } from 'components/settingsModal' +import { TerminalComponent } from 'components/terminal' @NgModule({ @@ -33,7 +28,6 @@ import { SettingsModalComponent } from 'components/settingsModal' FormsModule, ToasterModule, NgbModule.forRoot(), - PerfectScrollbarModule.forRoot(PERFECT_SCROLLBAR_CONFIG), ], providers: [ ConfigService, @@ -43,6 +37,7 @@ import { SettingsModalComponent } from 'components/settingsModal' ModalService, NotifyService, QuitterService, + SessionsService, LocalStorageService, ], entryComponents: [ @@ -52,6 +47,7 @@ import { SettingsModalComponent } from 'components/settingsModal' AppComponent, CheckboxComponent, SettingsModalComponent, + TerminalComponent, ], bootstrap: [ AppComponent diff --git a/app/src/components/app.pug b/app/src/components/app.pug index 0e6c983f..1ba5872e 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -3,14 +3,16 @@ div.navbar.navbar-default.draggable | × button.btn.btn-default.navbar-btn.navbar-btn-big.pull-right((click)='showSettings()', title='Settings') i.fa.fa-cog - div.navbar-brand - img.logo(src=require("img/logo.svg")) - -perfect-scrollbar - div.container - div#term(style='width: 300px; height: 300px;') +ngb-tabset + ngb-tab(*ngFor='let tab of tabs; trackBy: tab?.name') + template(ngbTabTitle) + span {{tab.name}} + button.btn.btn-default((click)='closeTab(tab)') × + template(ngbTabContent) + terminal([session]='tab', style='width: 300px; height: 300px;') +button.btn.btn-default((click)='newTab()') New tab footer toaster-container([toasterconfig]="toasterconfig") diff --git a/app/src/components/app.ts b/app/src/components/app.ts index 55bda90f..f593ab61 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -5,15 +5,13 @@ import { HostAppService } from 'services/hostApp' import { LogService } from 'services/log' import { QuitterService } from 'services/quitter' import { ToasterConfig } from 'angular2-toaster' +import { Session, SessionsService } from 'services/sessions' import { SettingsModalComponent } from 'components/settingsModal' import 'angular2-toaster/lib/toaster.css' import 'global.less' -const hterm = require('hterm-commonjs') -var pty = require('pty.js'); - @Component({ selector: 'app', @@ -25,6 +23,7 @@ export class AppComponent { private hostApp: HostAppService, private modal: ModalService, private electron: ElectronService, + private sessions: SessionsService, element: ElementRef, log: LogService, _quitter: QuitterService, @@ -42,40 +41,17 @@ export class AppComponent { } toasterConfig: ToasterConfig + tabs: Session[] = [] + + newTab () { + this.tabs.push(this.sessions.createSession({command: 'zsh'})) + } + + closeTab (session) { + session.destroy() + } ngOnInit () { - let io - hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() - let t = new hterm.hterm.Terminal() - t.onTerminalReady = function() { - t.installKeyboard() - io = t.io.push(); - //#t.decorate(element.nativeElement); - - var cmd = pty.spawn('bash', [], { - name: 'xterm-color', - cols: 80, - rows: 30, - cwd: process.env.HOME, - env: process.env - }); - cmd.on('data', function(data) { - io.writeUTF8(data); - }); - - - io.onVTKeystroke = function(str) { - cmd.write(str) - }; - io.sendString = function(str) { - cmd.write(str) - }; - io.onTerminalResize = function(columns, rows) { - cmd.resize(columns, rows) - }; - }; - console.log(document.querySelector('#term')) - t.decorate(document.querySelector('#term')); } ngOnDestroy () { diff --git a/app/src/components/settingsModal.pug b/app/src/components/settingsModal.pug index 2cda7c22..5dff65e2 100644 --- a/app/src/components/settingsModal.pug +++ b/app/src/components/settingsModal.pug @@ -19,21 +19,6 @@ div.modal-body .title Server .value Not connected - .status-line(*ngIf='!userInfo?.user') - .icon - img(src=require("img/user.png")) - .main - .title Login - .value Not logged in - - .status-line(*ngIf='userInfo?.user') - .icon - img([src]='userInfo.user.avatar || "../../assets/img/user.png"') - .main - .title Login - .value {{ userInfo.user.full_name || userInfo.user.username }} - - br div.form-group checkbox(text='Remember connected workspaces', '[(model)]'='config.store.rememberWorkspaces') diff --git a/app/src/components/terminal.less b/app/src/components/terminal.less new file mode 100644 index 00000000..306dd453 --- /dev/null +++ b/app/src/components/terminal.less @@ -0,0 +1,5 @@ +:host { + position: relative; + display: block; + overflow: hidden; +} diff --git a/app/src/components/terminal.ts b/app/src/components/terminal.ts new file mode 100644 index 00000000..b0461430 --- /dev/null +++ b/app/src/components/terminal.ts @@ -0,0 +1,55 @@ +import { Component, Input, ElementRef } from '@angular/core' +import { ElectronService } from 'services/electron' +import { ConfigService } from 'services/config' + +import { Session } from 'services/sessions' + +const hterm = require('hterm-commonjs') + + +@Component({ + selector: 'terminal', + template: '', + styles: [require('./terminal.less')], +}) +export class TerminalComponent { + @Input() session: Session + private terminal: any + + constructor( + private electron: ElectronService, + private elementRef: ElementRef, + public config: ConfigService, + ) { + } + + ngOnInit () { + let io + hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() + this.terminal = new hterm.hterm.Terminal() + this.terminal.onTerminalReady = () => { + this.terminal.installKeyboard() + io = this.terminal.io.push() + const dataSubscription = this.session.dataAvailable.subscribe((data) => { + io.writeUTF8(data) + }) + const closedSubscription = this.session.closed.subscribe(() => { + dataSubscription.unsubscribe() + closedSubscription.unsubscribe() + }) + io.onVTKeystroke = (str) => { + this.session.write(str) + } + io.sendString = (str) => { + this.session.write(str) + } + io.onTerminalResize = (columns, rows) => { + this.session.resize(columns, rows) + } + } + this.terminal.decorate(this.elementRef.nativeElement) + } + + ngOnDestroy () { + } +} diff --git a/app/src/global.less b/app/src/global.less index 71093e88..3edc6c73 100644 --- a/app/src/global.less +++ b/app/src/global.less @@ -67,41 +67,6 @@ body { } } -.avatar { - margin: 20px; - width: 100px; - height: 100px; - border-radius: 50px; - box-shadow: 0 1px 1px rgba(0,0,0,.5); -} - - - -.fa-live { - color: #7aff00; - - .list-group-item &.fa-2x { - top: 10px; - position: relative; - } -} - - -.otp-input { - height: 50px; - - input { - height: 50px; - font-size: 41px; - font-family: monospace; - text-align: center; - } - - .btn { - height: 50px; - width: 50px; - } -} ngb-modal-backdrop { @@ -169,8 +134,3 @@ ngb-tabset { } } } - -.ps-container.ps-in-scrolling>.ps-scrollbar-y-rail, -.ps-container:hover>.ps-scrollbar-y-rail:hover { - background: rgba(0,0,0,.5) !important; -} diff --git a/app/src/services/sessions.ts b/app/src/services/sessions.ts new file mode 100644 index 00000000..5ccb2159 --- /dev/null +++ b/app/src/services/sessions.ts @@ -0,0 +1,111 @@ +import { Injectable, NgZone, EventEmitter } from '@angular/core' +import { Logger, LogService } from 'services/log' +import * as ptyjs from 'pty.js' + + +export interface SessionOptions { + name?: string, + command: string, + cwd?: string, + env?: string, +} + +export class Session { + open: boolean + name: string + pty: any + dataAvailable = new EventEmitter() + closed = new EventEmitter() + destroyed = new EventEmitter() + + constructor (options: SessionOptions) { + this.name = options.name + this.pty = ptyjs.spawn('sh', ['-c', options.command], { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: options.cwd || process.env.HOME, + env: options.env || process.env, + }) + + this.open = true + + this.pty.on('data', (data) => { + this.dataAvailable.emit(data) + }) + + this.pty.on('close', () => { + this.open = false + this.closed.emit() + }) + } + + resize (columns, rows) { + this.pty.resize(columns, rows) + } + + write (data) { + this.pty.write(data) + } + + sendSignal (signal) { + this.pty.kill(signal) + } + + close () { + this.open = false + this.closed.emit() + this.pty.end() + } + + gracefullyDestroy () { + return new Promise((resolve) => { + this.sendSignal('SIGTERM') + if (!open) { + resolve() + this.destroy() + } else { + setTimeout(() => { + if (this.open) { + this.sendSignal('SIGKILL') + this.destroy() + } + resolve() + }, 1000) + } + }) + } + + destroy () { + if (open) { + this.close() + } + this.destroyed.emit() + this.pty.destroy() + } +} + +@Injectable() +export class SessionsService { + sessions: {[id: string]: Session} = {} + logger: Logger + private lastID = 0 + + constructor( + log: LogService, + ) { + this.logger = log.create('sessions') + } + + createSession (options: SessionOptions) : Session { + this.lastID++ + options.name = `session-${this.lastID}` + let session = new Session(options) + const destroySubscription = session.destroyed.subscribe(() => { + delete this.sessions[session.name] + destroySubscription.unsubscribe() + }) + this.sessions[session.name] = session + return session + } +} diff --git a/typings.json b/typings.json index 6cdf6704..d2417300 100644 --- a/typings.json +++ b/typings.json @@ -4,5 +4,8 @@ "electron": "registry:dt/electron#1.3.3+20161012142539", "jquery": "registry:dt/jquery#1.10.0+20160929162922", "node": "registry:dt/node#6.0.0+20161014191813" + }, + "dependencies": { + "pty.js": "registry:dt/pty.js#0.2.7-1+20161128184045" } }