This commit is contained in:
Eugene Pankov 2017-03-20 17:46:25 +01:00
parent 2c2da1d697
commit f659a45532
19 changed files with 174 additions and 79 deletions

View File

@ -1,12 +1,23 @@
const electron = require('electron')
let app = electron.app
let secondInstance = app.makeSingleInstance((argv) => {
app.window.webContents.send('host:second-instance')
})
if (secondInstance) {
app.quit()
return
}
const yaml = require('js-yaml') const yaml = require('js-yaml')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const Config = require('electron-config') const Config = require('electron-config')
const electron = require('electron')
const platform = require('os').platform() const platform = require('os').platform()
require('electron-debug')({enabled: true, showDevTools: process.argv.indexOf('--debug') != -1}) require('electron-debug')({enabled: true, showDevTools: process.argv.indexOf('--debug') != -1})
let app = electron.app
let windowConfig = new Config({name: 'window'}) let windowConfig = new Config({name: 'window'})
@ -14,7 +25,8 @@ setupWindowManagement = () => {
let windowCloseable let windowCloseable
app.window.on('show', () => { app.window.on('show', () => {
electron.ipcMain.send('window-shown') app.window.focus()
app.window.webContents.send('host:window-shown')
}) })
app.window.on('close', (e) => { app.window.on('close', (e) => {
@ -46,6 +58,14 @@ setupWindowManagement = () => {
}) })
electron.ipcMain.on('window-maximize', () => { electron.ipcMain.on('window-maximize', () => {
app.window.maximize()
})
electron.ipcMain.on('window-unmaximize', () => {
app.window.unmaximize()
})
electron.ipcMain.on('window-toggle-maximize', () => {
if (app.window.isMaximized()) { if (app.window.isMaximized()) {
app.window.unmaximize() app.window.unmaximize()
} else { } else {
@ -61,6 +81,10 @@ setupWindowManagement = () => {
app.window.setBounds(bounds, true) app.window.setBounds(bounds, true)
}) })
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
app.window.setAlwaysOnTop(flag)
})
app.on('before-quit', () => windowCloseable = true) app.on('before-quit', () => windowCloseable = true)
} }
@ -95,15 +119,6 @@ setupMenu = () => {
start = () => { start = () => {
let t0 = Date.now() let t0 = Date.now()
let secondInstance = app.makeSingleInstance((argv) => {
app.window.focus()
})
if (secondInstance) {
app.quit()
return
}
let configPath = path.join(electron.app.getPath('userData'), 'config.yaml') let configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
let configData let configData
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
@ -123,6 +138,7 @@ start = () => {
//- background to avoid the flash of unstyled window //- background to avoid the flash of unstyled window
backgroundColor: '#1D272D', backgroundColor: '#1D272D',
frame: false, frame: false,
type: 'toolbar',
} }
Object.assign(options, windowConfig.get('windowBoundaries')) Object.assign(options, windowConfig.get('windowBoundaries'))

View File

@ -3,15 +3,15 @@
"version": "1.0.0", "version": "1.0.0",
"main": "main.js", "main": "main.js",
"dependencies": { "dependencies": {
"child-process-promise": "^2.2.0", "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",
"electron-is-dev": "^0.1.2", "electron-is-dev": "0.1.2",
"path": "^0.12.7", "node-pty": "0.6.3",
"pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642" "path": "0.12.7"
}, },
"devDependencies": { "devDependencies": {
"js-yaml": "^3.8.2" "js-yaml": "3.8.2"
} }
} }

View File

@ -16,7 +16,6 @@ import { PluginDispatcherService } from 'services/pluginDispatcher'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { SessionsService } from 'services/sessions' import { SessionsService } from 'services/sessions'
import { DockingService } from 'services/docking' import { DockingService } from 'services/docking'
import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter'
import { AppComponent } from 'components/app' import { AppComponent } from 'components/app'
import { CheckboxComponent } from 'components/checkbox' import { CheckboxComponent } from 'components/checkbox'
@ -48,7 +47,6 @@ import { TerminalComponent } from 'components/terminal'
PluginDispatcherService, PluginDispatcherService,
QuitterService, QuitterService,
SessionsService, SessionsService,
LocalStorageService,
], ],
entryComponents: [ entryComponents: [
HotkeyInputModalComponent, HotkeyInputModalComponent,

View File

@ -185,6 +185,7 @@
&.active { &.active {
background: @title-bg; background: @title-bg;
box-shadow: 0px -1px 0px 0px blue;
.content-wrapper { .content-wrapper {
//border-bottom: 2px solid #69bbea; //border-bottom: 2px solid #69bbea;

View File

@ -1,8 +1,8 @@
.titlebar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"') .titlebar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"')
.title((dblclick)='hostApp.maximizeWindow()') Term .title((dblclick)='hostApp.toggleMaximize()') Term
button.btn.btn-secondary.btn-minimize((click)='hostApp.minimizeWindow()') button.btn.btn-secondary.btn-minimize((click)='hostApp.minimize()')
i.fa.fa-window-minimize i.fa.fa-window-minimize
button.btn.btn-secondary.btn-maximize((click)='hostApp.maximizeWindow()') button.btn.btn-secondary.btn-maximize((click)='hostApp.toggleMaximize()')
i.fa.fa-window-maximize i.fa.fa-window-maximize
button.btn.btn-secondary.btn-close((click)='hostApp.quit()') button.btn.btn-secondary.btn-close((click)='hostApp.quit()')
i.fa.fa-close i.fa.fa-close

View File

@ -66,11 +66,11 @@ export class AppComponent {
private elementRef: ElementRef, private elementRef: ElementRef,
private sessions: SessionsService, private sessions: SessionsService,
private docking: DockingService, private docking: DockingService,
private electron: ElectronService,
public hostApp: HostAppService, public hostApp: HostAppService,
public hotkeys: HotkeysService, public hotkeys: HotkeysService,
public config: ConfigService, public config: ConfigService,
log: LogService, log: LogService,
electron: ElectronService,
_quitter: QuitterService, _quitter: QuitterService,
) { ) {
console.timeStamp('AppComponent ctor') console.timeStamp('AppComponent ctor')
@ -135,10 +135,33 @@ export class AppComponent {
this.hostApp.shown.subscribe(() => { this.hostApp.shown.subscribe(() => {
this.docking.dock() this.docking.dock()
}) })
this.hostApp.secondInstance.subscribe(() => {
if (this.electron.app.window.isFocused()) {
// focused
this.electron.app.window.hide()
} else {
if (!this.electron.app.window.isVisible()) {
// unfocused, invisible
this.electron.app.window.show()
} else {
if (this.config.full().appearance.dock == 'off') {
// not docked, visible
setTimeout(() => {
this.electron.app.window.focus()
})
} else {
// docked, visible
this.electron.app.window.hide()
}
}
}
this.docking.dock()
})
} }
newTab () { newTab () {
this.addTerminalTab(this.sessions.createNewSession({command: 'bash'})) this.addTerminalTab(this.sessions.createNewSession({shell: 'zsh'}))
} }
addTerminalTab (session) { addTerminalTab (session) {

View File

@ -31,7 +31,7 @@ ngb-tabset(type='tabs')
label Dock the terminal label Dock the terminal
br br
.row .row
.col-auto .col.col-auto
div( div(
'[(ngModel)]'='config.store.appearance.dock' '[(ngModel)]'='config.store.appearance.dock'
'(ngModelChange)'='config.save(); docking.dock()' '(ngModelChange)'='config.save(); docking.dock()'
@ -71,10 +71,10 @@ ngb-tabset(type='tabs')
input( input(
type='range', type='range',
'[(ngModel)]'='config.store.appearance.dockFill', '[(ngModel)]'='config.store.appearance.dockFill',
'(ngModelChange)'='config.save(); docking.dock()', '(mouseup)'='config.save(); docking.dock()',
min='1', min='0.05',
max='100', max='1',
step='1' step='0.01'
) )
br br
div( div(

View File

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

View File

@ -0,0 +1,10 @@
:host {
position: relative;
display: block;
overflow: hidden;
div[style]:last-child {
background: black !important;
color: white !important;
}
}

View File

@ -25,19 +25,10 @@ hterm.hterm.VT.ESC['k'] = function(parseState) {
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory() hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
const preferenceManager = new hterm.hterm.PreferenceManager('default') const preferenceManager = new hterm.hterm.PreferenceManager('default')
preferenceManager.set('user-css', dataurl.convert({ preferenceManager.set('user-css', dataurl.convert({
data: ` data: require('./terminal.userCSS.scss'),
a {
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
`,
mimetype: 'text/css', mimetype: 'text/css',
charset: 'utf8', charset: 'utf8',
})) }))
preferenceManager.set('font-size', 12)
preferenceManager.set('background-color', '#1D272D') preferenceManager.set('background-color', '#1D272D')
preferenceManager.set('color-palette-overrides', { preferenceManager.set('color-palette-overrides', {
0: '#1D272D', 0: '#1D272D',
@ -49,11 +40,12 @@ hterm.hterm.ScrollPort.prototype.decorate = function (...args) {
this.screen_.style.cssText += `; padding-right: ${this.screen_.offsetWidth - this.screen_.clientWidth}px;` this.screen_.style.cssText += `; padding-right: ${this.screen_.offsetWidth - this.screen_.clientWidth}px;`
} }
hterm.hterm.Terminal.prototype.showOverlay = () => null
@Component({ @Component({
selector: 'terminal', selector: 'terminal',
template: '', template: '',
styles: [require('./terminal.less')], styles: [require('./terminal.scss')],
}) })
export class TerminalComponent { export class TerminalComponent {
@Input() session: Session @Input() session: Session
@ -115,6 +107,7 @@ export class TerminalComponent {
preferenceManager.set('font-size', config.appearance.fontSize) preferenceManager.set('font-size', config.appearance.fontSize)
preferenceManager.set('audible-bell-sound', '') preferenceManager.set('audible-bell-sound', '')
preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification') preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification')
preferenceManager.set('enable-clipboard-notice', false)
} }
ngOnDestroy () { ngOnDestroy () {

View File

@ -0,0 +1,11 @@
a {
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
* {
font-feature-settings: "liga" 0; // disable ligatures (they break monospacing)
}

View File

@ -14,6 +14,7 @@ export interface IAppearanceData {
fontSize: number fontSize: number
dock: string dock: string
dockScreen: string dockScreen: string
dockFill: number
} }
export interface ITerminalData { export interface ITerminalData {

View File

@ -26,18 +26,19 @@ export class DockingService {
let dockSide = this.config.full().appearance.dock let dockSide = this.config.full().appearance.dock
let newBounds: Electron.Rectangle = { x: 0, y: 0, width: 0, height: 0 } let newBounds: Electron.Rectangle = { x: 0, y: 0, width: 0, height: 0 }
let fill = 0.5 let fill = this.config.full().appearance.dockFill
if (dockSide == 'off') { if (dockSide == 'off') {
this.hostApp.setAlwaysOnTop(false)
return return
} }
if (dockSide == 'left' || dockSide == 'right') { if (dockSide == 'left' || dockSide == 'right') {
newBounds.width = fill * display.bounds.width newBounds.width = Math.round(fill * display.bounds.width)
newBounds.height = display.bounds.height newBounds.height = display.bounds.height
} }
if (dockSide == 'top' || dockSide == 'bottom') { if (dockSide == 'top' || dockSide == 'bottom') {
newBounds.width = display.bounds.width newBounds.width = display.bounds.width
newBounds.height = fill * display.bounds.height newBounds.height = Math.round(fill * display.bounds.height)
} }
if (dockSide == 'right') { if (dockSide == 'right') {
newBounds.x = display.bounds.x + display.bounds.width * (1.0 - fill) newBounds.x = display.bounds.x + display.bounds.width * (1.0 - fill)
@ -50,6 +51,8 @@ export class DockingService {
newBounds.y = display.bounds.y newBounds.y = display.bounds.y
} }
this.hostApp.setAlwaysOnTop(true)
this.hostApp.unmaximize()
this.hostApp.setBounds(newBounds) this.hostApp.setBounds(newBounds)
} }

View File

@ -23,10 +23,14 @@ export class HostAppService {
console.error('Unhandled exception:', err) console.error('Unhandled exception:', err)
}) })
electron.ipcRenderer.on('window-shown', () => { electron.ipcRenderer.on('host:window-shown', () => {
this.shown.emit() this.shown.emit()
}) })
electron.ipcRenderer.on('host:second-instance', () => {
this.secondInstance.emit()
})
this.ready.subscribe(() => { this.ready.subscribe(() => {
electron.ipcRenderer.send('app:ready') electron.ipcRenderer.send('app:ready')
}) })
@ -36,6 +40,7 @@ export class HostAppService {
quitRequested = new EventEmitter<any>() quitRequested = new EventEmitter<any>()
ready = new EventEmitter<any>() ready = new EventEmitter<any>()
shown = new EventEmitter<any>() shown = new EventEmitter<any>()
secondInstance = new EventEmitter<any>()
private logger: Logger; private logger: Logger;
@ -59,8 +64,8 @@ export class HostAppService {
this.electron.app.webContents.openDevTools() this.electron.app.webContents.openDevTools()
} }
setWindowCloseable(flag: boolean) { setCloseable(flag: boolean) {
this.electron.ipcRenderer.send('window-closeable', flag) this.electron.ipcRenderer.send('window-set-closeable', flag)
} }
focusWindow() { focusWindow() {
@ -71,18 +76,30 @@ export class HostAppService {
this.electron.ipcRenderer.send('window-toggle-focus') this.electron.ipcRenderer.send('window-toggle-focus')
} }
minimizeWindow () { minimize () {
this.electron.ipcRenderer.send('window-minimize') this.electron.ipcRenderer.send('window-minimize')
} }
maximizeWindow () { maximize () {
this.electron.ipcRenderer.send('window-maximize') this.electron.ipcRenderer.send('window-maximize')
} }
unmaximize () {
this.electron.ipcRenderer.send('window-unmaximize')
}
toggleMaximize () {
this.electron.ipcRenderer.send('window-toggle-maximize')
}
setBounds (bounds: Electron.Rectangle) { setBounds (bounds: Electron.Rectangle) {
this.electron.ipcRenderer.send('window-set-bounds', bounds) this.electron.ipcRenderer.send('window-set-bounds', bounds)
} }
setAlwaysOnTop (flag: boolean) {
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
}
quit () { quit () {
this.logger.info('Quitting') this.logger.info('Quitting')
this.electron.app.quit() this.electron.app.quit()

View File

@ -13,7 +13,7 @@ export class QuitterService {
} }
quit() { quit() {
this.hostApp.setWindowCloseable(true) this.hostApp.setCloseable(true)
this.hostApp.quit() this.hostApp.quit()
} }
} }

View File

@ -2,7 +2,8 @@ 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 const exec = require('child-process-promise').exec
import * as crypto from 'crypto' import * as crypto from 'crypto'
import * as ptyjs from 'pty.js' import * as nodePTY from 'node-pty'
import * as fs from 'fs'
export interface SessionRecoveryProvider { export interface SessionRecoveryProvider {
@ -42,16 +43,26 @@ export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
getNewSessionCommand(command: string): string { getNewSessionCommand(command: string): string {
const id = crypto.randomBytes(8).toString('hex') const id = crypto.randomBytes(8).toString('hex')
return `screen -U -S term-tab-${id} -- ${command}` // TODO
let configPath = '/tmp/.termScreenConfig'
fs.writeFileSync(configPath, `
escape ^^^
vbell off
term xterm-color
bindkey "^[OH" beginning-of-line
bindkey "^[OF" end-of-line
`, 'utf-8')
return `screen -c ${configPath} -U -S term-tab-${id} -- ${command}`
} }
} }
export interface SessionOptions { export interface SessionOptions {
name?: string, name?: string,
command: string, command?: string,
shell?: string,
cwd?: string, cwd?: string,
env?: string, env?: any,
} }
export class Session { export class Session {
@ -67,14 +78,22 @@ export class Session {
constructor (options: SessionOptions) { constructor (options: SessionOptions) {
this.name = options.name this.name = options.name
console.log('Spawning', options.command) console.log('Spawning', options.command)
this.pty = ptyjs.spawn('sh', ['-c', options.command], {
name: 'screen-256color', let binary = options.shell || 'sh'
//name: 'xterm-256color', let args = options.shell ? [] : ['-c', options.command]
let env = {
...process.env,
...options.env,
TERM: 'xterm-256color',
}
this.pty = nodePTY.spawn(binary, args, {
//name: 'screen-256color',
name: 'xterm-256color',
//name: 'xterm-color', //name: 'xterm-color',
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: options.cwd || process.env.HOME, cwd: options.cwd || process.env.HOME,
env: options.env || process.env, env: env,
}) })
this.open = true this.open = true
@ -156,8 +175,8 @@ export class SessionsService {
log: LogService, log: LogService,
) { ) {
this.logger = log.create('sessions') this.logger = log.create('sessions')
//this.recoveryProvider = new ScreenSessionRecoveryProvider() this.recoveryProvider = new ScreenSessionRecoveryProvider()
this.recoveryProvider = new NullSessionRecoveryProvider() //this.recoveryProvider = new NullSessionRecoveryProvider()
} }
createNewSession (options: SessionOptions) : Session { createNewSession (options: SessionOptions) : Session {

View File

@ -4,13 +4,12 @@
"apply-loader": "^0.1.0", "apply-loader": "^0.1.0",
"autoprefixer": "^6.7.7", "autoprefixer": "^6.7.7",
"awesome-typescript-loader": "3.0.8", "awesome-typescript-loader": "3.0.8",
"bootstrap": "^4.0.0-alpha.6",
"css-loader": "0.26.1", "css-loader": "0.26.1",
"dataurl": "^0.1.0", "dataurl": "^0.1.0",
"electron": "^1.4.13", "electron": "1.6.2",
"electron-builder": "10.6.1", "electron-builder": "10.6.1",
"electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04", "electron-osx-sign": "electron-userland/electron-osx-sign#f092181a1bffa2b3248a23ee28447a47e14a8f04",
"electron-rebuild": "1.4.0", "electron-rebuild": "1.5.7",
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"html-loader": "^0.4.4", "html-loader": "^0.4.4",
@ -69,14 +68,12 @@
"@angular/router": "3.3.1", "@angular/router": "3.3.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.15", "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.15",
"@types/core-js": "^0.9.35", "@types/core-js": "^0.9.35",
"@types/electron": "^1.4.33", "@types/electron": "1.4.34",
"@types/js-yaml": "^3.5.29", "@types/js-yaml": "^3.5.29",
"@types/node": "^7.0.5", "@types/node": "^7.0.5",
"@types/pty.js": "^0.2.32",
"angular2-localstorage": "github:AilisObrian/angular2-localstorage",
"angular2-perfect-scrollbar": "^1.1.0", "angular2-perfect-scrollbar": "^1.1.0",
"angular2-toaster": "^1.1.0", "angular2-toaster": "^1.1.0",
"bootstrap": "^3.3.7", "bootstrap": "4.0.0-alpha.6",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"deepmerge": "^1.3.2", "deepmerge": "^1.3.2",
"hterm-commonjs": "^1.0.0", "hterm-commonjs": "^1.0.0",

View File

@ -13,7 +13,12 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noUnusedLocals": true "noUnusedLocals": true,
"lib": [
"dom",
"es2015",
"es7"
]
}, },
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": [

View File

@ -4,7 +4,7 @@ const webpack = require("webpack")
module.exports = { module.exports = {
target: 'node', target: 'node',
entry: { entry: {
'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!./app/index.pug', 'index.ignore': 'file-loader?name=index.html!pug-html-loader!./app/index.pug',
'preload': './app/src/entry.preload.ts', 'preload': './app/src/entry.preload.ts',
'bundle': './app/src/entry.ts', 'bundle': './app/src/entry.ts',
}, },
@ -52,7 +52,13 @@ module.exports = {
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'] use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: [/app\/src\/components\//],
},
{
test: /\.scss$/,
use: ['to-string-loader', 'css-loader', 'sass-loader'],
include: [/app\/src\/components\//],
}, },
{ {
test: /\.(png|svg)$/, test: /\.(png|svg)$/,
@ -83,7 +89,7 @@ module.exports = {
'shell': 'require("shell")', 'shell': 'require("shell")',
'ipc': 'require("ipc")', 'ipc': 'require("ipc")',
'crypto': 'require("crypto")', 'crypto': 'require("crypto")',
'pty.js': 'require("pty.js")', 'node-pty': 'require("node-pty")',
'child-process-promise': 'require("child-process-promise")', 'child-process-promise': 'require("child-process-promise")',
}, },
plugins: [ plugins: [