mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-25 13:59:52 +00:00
.
This commit is contained in:
parent
4e451d0598
commit
d7bae654eb
19
app/main.js
19
app/main.js
@ -13,7 +13,7 @@ setupWindowManagement = () => {
|
|||||||
app.window.on('close', (e) => {
|
app.window.on('close', (e) => {
|
||||||
windowConfig.set('windowBoundaries', app.window.getBounds())
|
windowConfig.set('windowBoundaries', app.window.getBounds())
|
||||||
if (!windowCloseable) {
|
if (!windowCloseable) {
|
||||||
app.window.hide()
|
app.window.minimize()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -31,6 +31,18 @@ setupWindowManagement = () => {
|
|||||||
app.window.focus()
|
app.window.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
electron.ipcMain.on('window-maximize', () => {
|
||||||
|
if (app.window.isMaximized()) {
|
||||||
|
app.window.unmaximize()
|
||||||
|
} else {
|
||||||
|
app.window.maximize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
electron.ipcMain.on('window-minimize', () => {
|
||||||
|
app.window.minimize()
|
||||||
|
})
|
||||||
|
|
||||||
app.on('before-quit', () => windowCloseable = true)
|
app.on('before-quit', () => windowCloseable = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,16 +94,15 @@ start = () => {
|
|||||||
'web-preferences': {'web-security': false},
|
'web-preferences': {'web-security': false},
|
||||||
//- background to avoid the flash of unstyled window
|
//- background to avoid the flash of unstyled window
|
||||||
backgroundColor: '#1D272D',
|
backgroundColor: '#1D272D',
|
||||||
|
frame: false,
|
||||||
}
|
}
|
||||||
Object.assign(options, windowConfig.get('windowBoundaries'))
|
Object.assign(options, windowConfig.get('windowBoundaries'))
|
||||||
|
|
||||||
if (platform == 'darwin') {
|
if (platform == 'darwin') {
|
||||||
options.titleBarStyle = 'hidden'
|
options.titleBarStyle = 'hidden'
|
||||||
} else {
|
|
||||||
options.frame = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.commandLine.appendSwitch('--disable-http-cache')
|
app.commandLine.appendSwitch('disable-http-cache')
|
||||||
|
|
||||||
app.window = new electron.BrowserWindow(options)
|
app.window = new electron.BrowserWindow(options)
|
||||||
app.window.loadURL(`file://${app.getAppPath()}/assets/webpack/index.html`, {extraHeaders: "pragma: no-cache\n"})
|
app.window.loadURL(`file://${app.getAppPath()}/assets/webpack/index.html`, {extraHeaders: "pragma: no-cache\n"})
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"child-process-promise": "^2.1.3",
|
"child-process-promise": "^2.2.0",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"electron-config": "^0.2.1",
|
"electron-config": "^0.2.1",
|
||||||
"electron-debug": "^1.0.1",
|
"electron-debug": "^1.0.1",
|
||||||
|
@ -9,6 +9,7 @@ import { ConfigService } from 'services/config'
|
|||||||
import { ElectronService } from 'services/electron'
|
import { ElectronService } from 'services/electron'
|
||||||
import { HostAppService } from 'services/hostApp'
|
import { HostAppService } from 'services/hostApp'
|
||||||
import { LogService } from 'services/log'
|
import { LogService } from 'services/log'
|
||||||
|
import { HotkeysService } from 'services/hotkeys'
|
||||||
import { ModalService } from 'services/modal'
|
import { ModalService } from 'services/modal'
|
||||||
import { NotifyService } from 'services/notify'
|
import { NotifyService } from 'services/notify'
|
||||||
import { QuitterService } from 'services/quitter'
|
import { QuitterService } from 'services/quitter'
|
||||||
@ -33,6 +34,7 @@ import { TerminalComponent } from 'components/terminal'
|
|||||||
ConfigService,
|
ConfigService,
|
||||||
ElectronService,
|
ElectronService,
|
||||||
HostAppService,
|
HostAppService,
|
||||||
|
HotkeysService,
|
||||||
LogService,
|
LogService,
|
||||||
ModalService,
|
ModalService,
|
||||||
NotifyService,
|
NotifyService,
|
||||||
|
@ -3,20 +3,65 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100vw;
|
width: ~"calc(100vw - 2px)";
|
||||||
height: 100vh;
|
height: ~"calc(100vh - 2px)";
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
cursor: default;
|
||||||
background: @body-bg;
|
background: @body-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@titlebar-height: 35px;
|
||||||
@tabs-height: 40px;
|
@tabs-height: 40px;
|
||||||
|
@tab-border-radius: 3px;
|
||||||
|
|
||||||
|
.button-states() {
|
||||||
|
transition: 0.125s all;
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background: rgba(255, 255, 255, .033);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(.active) {
|
||||||
|
background: rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar {
|
||||||
|
height: @titlebar-height;
|
||||||
|
background: #141c23;
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: auto;
|
||||||
|
padding-left: 15px;
|
||||||
|
line-height: @titlebar-height;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-minimize, .btn-maximize, .btn-close {
|
||||||
|
flex: none;
|
||||||
|
line-height: @titlebar-height - 2px;
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 8px;
|
||||||
|
|
||||||
|
.button-states();
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: @tabs-height;
|
height: @tabs-height;
|
||||||
|
background: #141c23;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -41,13 +86,7 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
.button-states();
|
||||||
background: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(0, 0, 0, .1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@ -55,24 +94,41 @@
|
|||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
background: @body-bg;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
div {
|
&.pre-selected, &:nth-last-child(2) {
|
||||||
|
border-top-right-radius: @tab-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post-selected {
|
||||||
|
border-top-left-radius: @tab-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.index {
|
||||||
|
flex: none;
|
||||||
|
padding: 0 0 0 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
padding: 0 15px;
|
margin: 0 15px 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
transition: 0.25s all;
|
transition: 0.25s all;
|
||||||
|
|
||||||
&:hover:not(.active) {
|
.button-states();
|
||||||
background: rgba(255, 255, 255, .05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(0, 0, 0, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: #141c23;
|
background: #141c23;
|
||||||
|
@ -1,6 +1,22 @@
|
|||||||
|
.titlebar
|
||||||
|
.title((dblclick)='hostApp.maximizeWindow()') Term
|
||||||
|
.btn-minimize((click)='hostApp.minimizeWindow()')
|
||||||
|
i.fa.fa-window-minimize
|
||||||
|
.btn-maximize((click)='hostApp.maximizeWindow()')
|
||||||
|
i.fa.fa-window-maximize
|
||||||
|
.btn-close((click)='hostApp.quit()')
|
||||||
|
i.fa.fa-close
|
||||||
|
|
||||||
.tabs
|
.tabs
|
||||||
.tab(*ngFor='let tab of tabs; trackBy: tab?.id', (click)='selectTab(tab)', [class.active]='tab == activeTab')
|
.tab(
|
||||||
div {{tab.name}}
|
*ngFor='let tab of tabs; let idx = index; trackBy: tab?.id',
|
||||||
|
(click)='selectTab(tab)',
|
||||||
|
[class.active]='tab == activeTab',
|
||||||
|
[class.pre-selected]='tabs[idx + 1] == activeTab',
|
||||||
|
[class.post-selected]='tabs[idx - 1] == activeTab',
|
||||||
|
)
|
||||||
|
div.index {{idx + 1}}
|
||||||
|
div.name {{tab.name || 'Terminal'}}
|
||||||
button((click)='closeTab(tab)') ×
|
button((click)='closeTab(tab)') ×
|
||||||
.btn-new-tab((click)='newTab()')
|
.btn-new-tab((click)='newTab()')
|
||||||
i.fa.fa-plus
|
i.fa.fa-plus
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, ElementRef } from '@angular/core'
|
||||||
import { ModalService } from 'services/modal'
|
import { ModalService } from 'services/modal'
|
||||||
import { ElectronService } from 'services/electron'
|
import { ElectronService } from 'services/electron'
|
||||||
import { HostAppService } from 'services/hostApp'
|
import { HostAppService } from 'services/hostApp'
|
||||||
|
import { HotkeysService } from 'services/hotkeys'
|
||||||
import { LogService } from 'services/log'
|
import { LogService } from 'services/log'
|
||||||
import { QuitterService } from 'services/quitter'
|
import { QuitterService } from 'services/quitter'
|
||||||
import { ToasterConfig } from 'angular2-toaster'
|
import { ToasterConfig } from 'angular2-toaster'
|
||||||
@ -31,11 +32,13 @@ class Tab {
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private hostApp: HostAppService,
|
|
||||||
private modal: ModalService,
|
private modal: ModalService,
|
||||||
private electron: ElectronService,
|
private elementRef: ElementRef,
|
||||||
private sessions: SessionsService,
|
private sessions: SessionsService,
|
||||||
|
public hostApp: HostAppService,
|
||||||
|
public hotkeys: HotkeysService,
|
||||||
log: LogService,
|
log: LogService,
|
||||||
|
electron: ElectronService,
|
||||||
_quitter: QuitterService,
|
_quitter: QuitterService,
|
||||||
) {
|
) {
|
||||||
console.timeStamp('AppComponent ctor')
|
console.timeStamp('AppComponent ctor')
|
||||||
@ -48,6 +51,22 @@ export class AppComponent {
|
|||||||
preventDuplicates: true,
|
preventDuplicates: true,
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.hotkeys.key.subscribe((key) => {
|
||||||
|
if (key.event == 'keydown') {
|
||||||
|
if (key.alt && key.key >= '1' && key.key <= '9') {
|
||||||
|
let index = key.key.charCodeAt(0) - '0'.charCodeAt(0) - 1
|
||||||
|
if (index < this.tabs.length) {
|
||||||
|
this.selectTab(this.tabs[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.alt && key.key == '0') {
|
||||||
|
if (this.tabs.length >= 10) {
|
||||||
|
this.selectTab(this.tabs[9])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toasterConfig: ToasterConfig
|
toasterConfig: ToasterConfig
|
||||||
@ -55,23 +74,40 @@ export class AppComponent {
|
|||||||
activeTab: Tab
|
activeTab: Tab
|
||||||
|
|
||||||
newTab () {
|
newTab () {
|
||||||
const tab = new Tab(this.sessions.createSession({command: 'bash'}))
|
this.addSessionTab(this.sessions.createNewSession({command: 'bash'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
addSessionTab (session) {
|
||||||
|
let tab = new Tab(session)
|
||||||
this.tabs.push(tab)
|
this.tabs.push(tab)
|
||||||
this.selectTab(tab)
|
this.selectTab(tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab (tab) {
|
selectTab (tab) {
|
||||||
this.activeTab = tab
|
this.activeTab = tab
|
||||||
|
setImmediate(() => {
|
||||||
|
this.elementRef.nativeElement.querySelector(':scope .tab.active iframe').focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTab (tab) {
|
closeTab (tab) {
|
||||||
tab.session.destroy()
|
tab.session.gracefullyDestroy()
|
||||||
this.tabs = this.tabs.filter((x) => x != tab)
|
this.tabs = this.tabs.filter((x) => x != tab)
|
||||||
this.selectTab(this.tabs[0])
|
if (tab == this.activeTab) {
|
||||||
|
this.selectTab(this.tabs[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.newTab()
|
this.sessions.recoverAll().then((recoveredSessions) => {
|
||||||
|
if (recoveredSessions.length > 0) {
|
||||||
|
recoveredSessions.forEach((session) => {
|
||||||
|
this.addSessionTab(session)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.newTab()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
|
@ -2,11 +2,8 @@ import { Component } from '@angular/core'
|
|||||||
import { ElectronService } from 'services/electron'
|
import { ElectronService } from 'services/electron'
|
||||||
import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp'
|
import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp'
|
||||||
import { ConfigService } from 'services/config'
|
import { ConfigService } from 'services/config'
|
||||||
import { QuitterService } from 'services/quitter'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
import * as os from 'os'
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-modal',
|
selector: 'settings-modal',
|
||||||
@ -16,10 +13,9 @@ import * as os from 'os'
|
|||||||
export class SettingsModalComponent {
|
export class SettingsModalComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private modalInstance: NgbActiveModal,
|
private modalInstance: NgbActiveModal,
|
||||||
private hostApp: HostAppService,
|
|
||||||
private electron: ElectronService,
|
|
||||||
private quitter: QuitterService,
|
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
|
hostApp: HostAppService,
|
||||||
|
electron: ElectronService,
|
||||||
) {
|
) {
|
||||||
this.isWindows = hostApp.platform == PLATFORM_WINDOWS
|
this.isWindows = hostApp.platform == PLATFORM_WINDOWS
|
||||||
this.isMac = hostApp.platform == PLATFORM_MAC
|
this.isMac = hostApp.platform == PLATFORM_MAC
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core'
|
import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core'
|
||||||
import { ElectronService } from 'services/electron'
|
|
||||||
import { ConfigService } from 'services/config'
|
import { ConfigService } from 'services/config'
|
||||||
|
|
||||||
import { Session } from 'services/sessions'
|
import { Session } from 'services/sessions'
|
||||||
@ -10,16 +9,25 @@ const hterm = require('hterm-commonjs')
|
|||||||
hterm.hterm.VT.ESC['k'] = function(parseState) {
|
hterm.hterm.VT.ESC['k'] = function(parseState) {
|
||||||
parseState.resetArguments();
|
parseState.resetArguments();
|
||||||
|
|
||||||
function parseOSC(parseState) {
|
function parseOSC(ps) {
|
||||||
if (!this.parseUntilStringTerminator_(parseState) || parseState.func == parseOSC) {
|
if (!this.parseUntilStringTerminator_(ps) || ps.func == parseOSC) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.terminal.setWindowTitle(parseState.args[0])
|
this.terminal.setWindowTitle(ps.args[0])
|
||||||
}
|
}
|
||||||
parseState.func = parseOSC
|
parseState.func = parseOSC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
|
||||||
|
hterm.hterm.PreferenceManager.defaultPreferences['user-css'] = ``
|
||||||
|
const oldDecorate = hterm.hterm.ScrollPort.prototype.decorate
|
||||||
|
hterm.hterm.ScrollPort.prototype.decorate = function (...args) {
|
||||||
|
oldDecorate.bind(this)(...args)
|
||||||
|
this.screen_.style.cssText += `; padding-right: ${this.screen_.offsetWidth - this.screen_.clientWidth}px;`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'terminal',
|
selector: 'terminal',
|
||||||
template: '',
|
template: '',
|
||||||
@ -33,7 +41,6 @@ export class TerminalComponent {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private electron: ElectronService,
|
|
||||||
private elementRef: ElementRef,
|
private elementRef: ElementRef,
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
) {
|
) {
|
||||||
@ -41,7 +48,6 @@ export class TerminalComponent {
|
|||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
let io
|
let io
|
||||||
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
|
|
||||||
this.terminal = new hterm.hterm.Terminal()
|
this.terminal = new hterm.hterm.Terminal()
|
||||||
this.terminal.setWindowTitle = (title) => {
|
this.terminal.setWindowTitle = (title) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
@ -72,5 +78,6 @@ export class TerminalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ if (nodeRequire('electron-is-dev')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.timeStamp('angular bootstrap started')
|
console.timeStamp('angular bootstrap started')
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||||
|
|
||||||
|
|
||||||
process.emitWarning = function () { console.log(arguments) }
|
(<any>process).emitWarning = function () { console.log(arguments) }
|
||||||
|
@ -62,7 +62,15 @@ export class HostAppService {
|
|||||||
this.electron.ipcRenderer.send('window-focus')
|
this.electron.ipcRenderer.send('window-focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
quit() {
|
minimizeWindow () {
|
||||||
|
this.electron.ipcRenderer.send('window-minimize')
|
||||||
|
}
|
||||||
|
|
||||||
|
maximizeWindow () {
|
||||||
|
this.electron.ipcRenderer.send('window-maximize')
|
||||||
|
}
|
||||||
|
|
||||||
|
quit () {
|
||||||
this.logger.info('Quitting')
|
this.logger.info('Quitting')
|
||||||
this.electron.app.quit()
|
this.electron.app.quit()
|
||||||
}
|
}
|
||||||
|
60
app/src/services/hotkeys.ts
Normal file
60
app/src/services/hotkeys.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||||
|
const hterm = require('hterm-commonjs')
|
||||||
|
|
||||||
|
|
||||||
|
export interface Key {
|
||||||
|
event: string,
|
||||||
|
alt: boolean,
|
||||||
|
ctrl: boolean,
|
||||||
|
cmd: boolean,
|
||||||
|
shift: boolean,
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HotkeysService {
|
||||||
|
key = new EventEmitter<Key>()
|
||||||
|
|
||||||
|
constructor(private zone: NgZone) {
|
||||||
|
let events = [
|
||||||
|
{
|
||||||
|
name: 'keydown',
|
||||||
|
htermHandler: 'onKeyDown_',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keypress',
|
||||||
|
htermHandler: 'onKeyPress_',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyup',
|
||||||
|
htermHandler: 'onKeyUp_',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
events.forEach((event) => {
|
||||||
|
document.addEventListener(event.name, (nativeEvent) => {
|
||||||
|
this.emitNativeEvent(event.name, nativeEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
let oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
|
||||||
|
const __this = this
|
||||||
|
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
|
||||||
|
__this.emitNativeEvent(event.name, nativeEvent)
|
||||||
|
oldHandler.bind(this)(nativeEvent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNativeEvent (name, nativeEvent) {
|
||||||
|
console.debug('Key', nativeEvent)
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.key.emit({
|
||||||
|
event: name,
|
||||||
|
alt: nativeEvent.altKey,
|
||||||
|
shift: nativeEvent.shiftKey,
|
||||||
|
cmd: nativeEvent.metaKey,
|
||||||
|
ctrl: nativeEvent.ctrlKey,
|
||||||
|
key: nativeEvent.key,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import { Injectable, NgZone } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ModalService {
|
export class ModalService {
|
||||||
constructor(
|
constructor(
|
||||||
private zone: NgZone,
|
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ToasterService } from 'angular2-toaster'
|
import { ToasterService } from 'angular2-toaster'
|
||||||
import { LogService } from 'services/log'
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotifyService {
|
export class NotifyService {
|
||||||
constructor(
|
constructor(
|
||||||
private toaster: ToasterService,
|
private toaster: ToasterService,
|
||||||
private log: LogService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
pop(options) {
|
pop(options) {
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { HostAppService } from 'services/hostApp'
|
import { HostAppService } from 'services/hostApp'
|
||||||
import { ElectronService } from 'services/electron'
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QuitterService {
|
export class QuitterService {
|
||||||
constructor(
|
constructor(
|
||||||
private electron: ElectronService,
|
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
) {
|
) {
|
||||||
hostApp.quitRequested.subscribe(() => {
|
hostApp.quitRequested.subscribe(() => {
|
||||||
|
@ -1,8 +1,52 @@
|
|||||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||||
import { Logger, LogService } from 'services/log'
|
import { Logger, LogService } from 'services/log'
|
||||||
|
const exec = require('child-process-promise').exec
|
||||||
|
import * as crypto from 'crypto'
|
||||||
import * as ptyjs from 'pty.js'
|
import * as ptyjs from 'pty.js'
|
||||||
|
|
||||||
|
|
||||||
|
export interface SessionRecoveryProvider {
|
||||||
|
list(): Promise<any[]>
|
||||||
|
getRecoveryCommand(item: any): string
|
||||||
|
getNewSessionCommand(command: string): string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NullSessionRecoveryProvider implements SessionRecoveryProvider {
|
||||||
|
list(): Promise<any[]> {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecoveryCommand(_: any): string {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewSessionCommand(command: string) {
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
|
||||||
|
list(): Promise<any[]> {
|
||||||
|
return exec('screen -ls').then((result) => {
|
||||||
|
return result.stdout.split('\n')
|
||||||
|
.filter((line) => /\bterm-tab-/.exec(line))
|
||||||
|
.map((line) => line.trim().split('.')[0])
|
||||||
|
}).catch(() => {
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecoveryCommand(item: any): string {
|
||||||
|
return `screen -r ${item}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewSessionCommand(command: string): string {
|
||||||
|
const id = crypto.randomBytes(8).toString('hex')
|
||||||
|
return `screen -U -S term-tab-${id} -- ${command}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SessionOptions {
|
export interface SessionOptions {
|
||||||
name?: string,
|
name?: string,
|
||||||
command: string,
|
command: string,
|
||||||
@ -20,9 +64,10 @@ export class Session {
|
|||||||
|
|
||||||
constructor (options: SessionOptions) {
|
constructor (options: SessionOptions) {
|
||||||
this.name = options.name
|
this.name = options.name
|
||||||
|
console.log('Spawning', options.command)
|
||||||
this.pty = ptyjs.spawn('sh', ['-c', options.command], {
|
this.pty = ptyjs.spawn('sh', ['-c', options.command], {
|
||||||
name: 'xterm-color',
|
//name: 'xterm-color',
|
||||||
//name: 'screen-256color',
|
name: 'xterm-256color',
|
||||||
cols: 80,
|
cols: 80,
|
||||||
rows: 30,
|
rows: 30,
|
||||||
cwd: options.cwd || process.env.HOME,
|
cwd: options.cwd || process.env.HOME,
|
||||||
@ -62,7 +107,7 @@ export class Session {
|
|||||||
gracefullyDestroy () {
|
gracefullyDestroy () {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.sendSignal('SIGTERM')
|
this.sendSignal('SIGTERM')
|
||||||
if (!open) {
|
if (!this.open) {
|
||||||
resolve()
|
resolve()
|
||||||
this.destroy()
|
this.destroy()
|
||||||
} else {
|
} else {
|
||||||
@ -91,11 +136,20 @@ export class SessionsService {
|
|||||||
sessions: {[id: string]: Session} = {}
|
sessions: {[id: string]: Session} = {}
|
||||||
logger: Logger
|
logger: Logger
|
||||||
private lastID = 0
|
private lastID = 0
|
||||||
|
recoveryProvider: SessionRecoveryProvider
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private zone: NgZone,
|
||||||
log: LogService,
|
log: LogService,
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('sessions')
|
this.logger = log.create('sessions')
|
||||||
|
this.recoveryProvider = new ScreenSessionRecoveryProvider()
|
||||||
|
//this.recoveryProvider = new NullSessionRecoveryProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewSession (options: SessionOptions) : Session {
|
||||||
|
options.command = this.recoveryProvider.getNewSessionCommand(options.command)
|
||||||
|
return this.createSession(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
createSession (options: SessionOptions) : Session {
|
createSession (options: SessionOptions) : Session {
|
||||||
@ -109,4 +163,15 @@ export class SessionsService {
|
|||||||
this.sessions[session.name] = session
|
this.sessions[session.name] = session
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recoverAll () : Promise<Session[]> {
|
||||||
|
return <Promise<Session[]>>(this.recoveryProvider.list().then((items) => {
|
||||||
|
return this.zone.run(() => {
|
||||||
|
return items.map((item) => {
|
||||||
|
const command = this.recoveryProvider.getRecoveryCommand(item)
|
||||||
|
return this.createSession({command})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"style-loader": "^0.13.1",
|
"style-loader": "^0.13.1",
|
||||||
"to-string-loader": "^1.1.5",
|
"to-string-loader": "^1.1.5",
|
||||||
"tslint": "4.0.2",
|
"tslint": "4.2.0",
|
||||||
"typescript": "2.1.1",
|
"typescript": "2.1.1",
|
||||||
"typings": "2.0.0",
|
"typings": "2.0.0",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^0.5.7",
|
||||||
|
@ -11,17 +11,20 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noUnusedLocals": true
|
||||||
},
|
},
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"platforms",
|
"platforms"
|
||||||
],
|
],
|
||||||
"files": [
|
"filesGlob" : [
|
||||||
"app/src/app.d.ts",
|
"app/src/*.ts",
|
||||||
"app/src/entry.ts",
|
"app/src/**/*.ts",
|
||||||
"typings/index.d.ts",
|
"!node_modules/**",
|
||||||
|
"!app/node_modules/**",
|
||||||
"node_modules/rxjs/Rx.d.ts"
|
"node_modules/rxjs/Rx.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,13 @@
|
|||||||
"semicolon": false,
|
"semicolon": false,
|
||||||
"no-inferrable-types": [true, "ignore-params"],
|
"no-inferrable-types": [true, "ignore-params"],
|
||||||
"curly": true,
|
"curly": true,
|
||||||
"no-duplicate-key": true,
|
|
||||||
"no-duplicate-variable": true,
|
"no-duplicate-variable": true,
|
||||||
"no-empty": true,
|
"no-empty": true,
|
||||||
"no-eval": true,
|
"no-eval": true,
|
||||||
"no-invalid-this": true,
|
"no-invalid-this": true,
|
||||||
"no-shadowed-variable": true,
|
"no-shadowed-variable": true,
|
||||||
"no-unreachable": true,
|
|
||||||
"no-unused-expression": true,
|
"no-unused-expression": true,
|
||||||
"no-unused-new": true,
|
"no-unused-new": true,
|
||||||
"no-unused-variable": true,
|
|
||||||
"no-use-before-declare": true,
|
"no-use-before-declare": true,
|
||||||
"no-var-keyword": true,
|
"no-var-keyword": true,
|
||||||
"new-parens": true
|
"new-parens": true
|
||||||
|
@ -67,16 +67,19 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
'electron': 'require("electron")',
|
'fs': 'require("fs")',
|
||||||
|
'buffer': 'require("buffer")',
|
||||||
|
'system': '{}',
|
||||||
|
'file': '{}',
|
||||||
|
|
||||||
'net': 'require("net")',
|
'net': 'require("net")',
|
||||||
|
'electron': 'require("electron")',
|
||||||
'remote': 'require("remote")',
|
'remote': 'require("remote")',
|
||||||
'shell': 'require("shell")',
|
'shell': 'require("shell")',
|
||||||
'ipc': 'require("ipc")',
|
'ipc': 'require("ipc")',
|
||||||
'fs': 'require("fs")',
|
'crypto': 'require("crypto")',
|
||||||
'buffer': 'require("buffer")',
|
|
||||||
'pty.js': 'require("pty.js")',
|
'pty.js': 'require("pty.js")',
|
||||||
'system': '{}',
|
'child-process-promise': 'require("child-process-promise")',
|
||||||
'file': '{}'
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user