Eugene Pankov 9 лет назад
Родитель
Сommit
8a02fd1708

BIN
app/assets/img/disk.icns


BIN
app/assets/img/disk.ico


BIN
app/assets/img/logo.png


+ 0 - 107
app/assets/img/logo.svg

@@ -1,107 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.91 r13725"
-   xml:space="preserve"
-   width="536.82501"
-   height="126.525"
-   viewBox="0 0 536.82501 126.525"
-   sodipodi:docname="elements_wortmarke+bildmarke_gelb+weiß_rz.svg"
-   inkscape:export-filename="/home/eugene/Downloads/logo.png"
-   inkscape:export-xdpi="42.677204"
-   inkscape:export-ydpi="42.677204"><metadata
-     id="metadata8"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
-     id="defs6" /><sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1845"
-     inkscape:window-height="1025"
-     id="namedview4"
-     showgrid="false"
-     inkscape:zoom="1.1586643"
-     inkscape:cx="143.54613"
-     inkscape:cy="-3.8338411"
-     inkscape:window-x="75"
-     inkscape:window-y="27"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10" /><g
-     id="g10"
-     inkscape:groupmode="layer"
-     inkscape:label="ink_ext_XXXXXX"
-     transform="matrix(1.25,0,0,-1.25,0,126.525)"><g
-       id="g12"
-       transform="scale(0.1,0.1)"><path
-         d="m 202.457,809.793 404.891,0 0,202.457 -404.891,0 0,-202.457 z"
-         style="fill:#fff200;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path14"
-         inkscape:connector-curvature="0" /><path
-         d="m 0,607.375 202.445,0 0,202.457 -202.445,0 0,-202.457 z"
-         style="fill:#fff200;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path16"
-         inkscape:connector-curvature="0" /><path
-         d="m 202.457,404.918 404.891,0 0,202.457 -404.891,0 0,-202.457 z"
-         style="fill:#fff200;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path18"
-         inkscape:connector-curvature="0" /><path
-         d="m 0,202.461 202.445,0 0,202.457 -202.445,0 0,-202.457 z"
-         style="fill:#fff200;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path20"
-         inkscape:connector-curvature="0" /><path
-         d="m 202.457,0 404.891,0 0,202.461 -404.891,0 0,-202.461 z"
-         style="fill:#fff200;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path22"
-         inkscape:connector-curvature="0" /><path
-         d="m 1072.96,482.414 -148.108,0 0,48.219 148.108,0 0,-48.219 z m 39.27,-234.23 -302.441,0 0,516.679 302.441,0 0,-48.218 -247.32,0 0,-420.243 247.32,0 0,-48.218"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path24"
-         inkscape:connector-curvature="0" /><path
-         d="m 1518.04,248.184 -187.4,0 0,48.218 187.4,0 0,-48.218 z m -247.34,0 -55.1,0 0,516.679 55.1,0 0,-516.679"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path26"
-         inkscape:connector-curvature="0" /><path
-         d="m 1875.61,482.414 -148.12,0 0,48.219 148.12,0 0,-48.219 z m 39.27,-234.23 -302.43,0 0,516.679 302.43,0 0,-48.218 -247.33,0 0,-420.243 247.33,0 0,-48.218"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path28"
-         inkscape:connector-curvature="0" /><path
-         d="m 2515.65,248.184 -55.12,0 0,333.425 55.12,0 0,-333.425 z m -432.66,0 -55.1,0 0,333.425 55.1,0 0,-333.425"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path30"
-         inkscape:connector-curvature="0" /><path
-         d="m 2899.41,482.414 -148.12,0 0,48.219 148.12,0 0,-48.219 z m 39.28,-234.23 -302.44,0 0,516.679 302.44,0 0,-48.218 -247.34,0 0,-420.243 247.34,0 0,-48.218"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path32"
-         inkscape:connector-curvature="0" /><path
-         d="m 3438.17,419.605 -55.11,0 0,345.258 55.11,0 0,-345.258 z m -334.12,-171.421 -55.12,0 0,344.457 55.12,0 0,-344.457 z m 336.1,19.675 -44.09,-28.949 -352.46,505.524 44.79,28.933 351.76,-505.508"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path34"
-         inkscape:connector-curvature="0" /><path
-         d="m 3732.38,248.184 -55.1,0 0,408.523 55.1,0 0,-408.523 z m 160.52,468.461 -376.14,0 0,48.218 376.14,0 0,-48.218"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path36"
-         inkscape:connector-curvature="0" /><path
-         d="m 4243.6,638.805 c -18.61,62.683 -73.02,92.308 -126.76,92.308 -56.49,0 -108.85,-33.754 -108.85,-95.761 0,-37.891 23.43,-75.786 73.02,-86.801 l 16.54,-4.141 -13.1,-48.91 -15.16,3.453 c -75.77,17.902 -115.04,76.461 -115.04,135.02 0,95.757 80.6,144.683 162.59,144.683 73.71,0 148.81,-39.273 171.54,-122.64 L 4243.6,638.805 Z M 4122.34,234.406 c -75.78,0 -153.63,38.571 -181.18,124.696 l 46.84,19.285 c 19.99,-66.137 75.79,-95.762 132.27,-95.762 63.39,0 121.26,37.203 121.26,104.027 0,46.856 -28.24,81.297 -83.37,92.313 l -13.77,2.758 13.08,48.91 19.99,-4.129 c 76.46,-15.848 117.12,-69.586 117.12,-137.777 0,-85.438 -68.9,-154.321 -172.24,-154.321"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path38"
-         inkscape:connector-curvature="0" /><path
-         d="m 2528.61,742.93 -44.08,35.597 -211.89,-224.117 -212.58,224.731 -44.08,-35.598 249.15,-263.246 7.51,-7.883 7.51,7.883 248.46,262.633"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         id="path40"
-         inkscape:connector-curvature="0" /></g></g></svg>

BIN
app/assets/img/user.png


+ 4 - 8
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

+ 8 - 6
app/src/components/app.pug

@@ -3,14 +3,16 @@ div.navbar.navbar-default.draggable
         | &times;
     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)') &times;
+        template(ngbTabContent)
+            terminal([session]='tab', style='width: 300px; height: 300px;')
 
+button.btn.btn-default((click)='newTab()') New tab
 footer
 
 toaster-container([toasterconfig]="toasterconfig")

+ 10 - 34
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[] = []
 
-    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);
-                    });
+    newTab () {
+        this.tabs.push(this.sessions.createSession({command: 'zsh'}))
+    }
 
+    closeTab (session) {
+        session.destroy()
+    }
 
-                    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'));
+    ngOnInit () {
     }
 
     ngOnDestroy () {

+ 0 - 15
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')

+ 5 - 0
app/src/components/terminal.less

@@ -0,0 +1,5 @@
+:host {
+    position: relative;
+    display: block;
+    overflow: hidden;
+}

+ 55 - 0
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 () {
+    }
+}

+ 0 - 40
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;
-}

+ 111 - 0
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
+    }
+}

+ 3 - 0
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"
   }
 }