allow starting commands in new tabs via CLI (fixes #304)

This commit is contained in:
Eugene Pankov
2018-08-26 17:35:04 +02:00
parent 3f8f87a141
commit 6cc20c3719
23 changed files with 1006 additions and 411 deletions

23
app/lib/cli.js Normal file
View File

@@ -0,0 +1,23 @@
import { app } from 'electron'
export function parseArgs (argv, cwd) {
if (argv[0].includes('node')) {
argv = argv.slice(1)
}
return require('yargs')
.usage('terminus [command] [arguments]')
.command('open [directory]', 'open a shell in a directory', {
directory: { type: 'string', 'default': cwd },
})
.command('run [command...]', 'run a command in the terminal', {
command: { type: 'string' },
})
.version('v', 'Show version and exit', app.getVersion())
.alias('d', 'debug')
.describe('d', 'Show DevTools on start')
.alias('h', 'help')
.help('h')
.strict()
.parse(argv.slice(1))
}

304
app/lib/index.js Normal file
View File

@@ -0,0 +1,304 @@
import { app, ipcMain, BrowserWindow, Menu, Tray, shell } from 'electron'
import * as path from 'path'
import electronDebug from 'electron-debug'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import './lru'
import { parseArgs } from './cli'
import ElectronConfig from 'electron-config'
if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0)
let electronVibrancy
if (process.platform !== 'linux') {
electronVibrancy = require('electron-vibrancy')
}
let windowConfig = new ElectronConfig({ name: 'window' })
if (!process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS = ''
}
const setWindowVibrancy = (enabled) => {
if (enabled && !app.window.vibrancyViewID) {
app.window.vibrancyViewID = electronVibrancy.SetVibrancy(app.window, 0)
} else if (!enabled && app.window.vibrancyViewID) {
electronVibrancy.RemoveView(app.window, app.window.vibrancyViewID)
app.window.vibrancyViewID = null
}
}
const setupTray = () => {
if (process.platform === 'darwin') {
app.tray = new Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`)
app.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`)
} else {
app.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
}
app.tray.on('click', () => {
app.window.show()
app.window.focus()
})
const contextMenu = Menu.buildFromTemplate([{
label: 'Show',
click () {
app.window.show()
app.window.focus()
},
}])
if (process.platform !== 'darwin') {
app.tray.setContextMenu(contextMenu)
}
app.tray.setToolTip(`Terminus ${app.getVersion()}`)
}
const setupWindowManagement = () => {
app.window.on('show', () => {
app.window.webContents.send('host:window-shown')
if (app.tray) {
app.tray.destroy()
app.tray = null
}
})
app.window.on('hide', () => {
if (!app.tray) {
setupTray()
}
})
app.window.on('enter-full-screen', () => app.window.webContents.send('host:window-enter-full-screen'))
app.window.on('leave-full-screen', () => app.window.webContents.send('host:window-leave-full-screen'))
app.window.on('close', () => {
windowConfig.set('windowBoundaries', app.window.getBounds())
})
app.window.on('closed', () => {
app.window = null
})
ipcMain.on('window-focus', () => {
app.window.focus()
})
ipcMain.on('window-maximize', () => {
app.window.maximize()
})
ipcMain.on('window-unmaximize', () => {
app.window.unmaximize()
})
ipcMain.on('window-toggle-maximize', () => {
if (app.window.isMaximized()) {
app.window.unmaximize()
} else {
app.window.maximize()
}
})
ipcMain.on('window-minimize', () => {
app.window.minimize()
})
ipcMain.on('window-set-bounds', (event, bounds) => {
app.window.setBounds(bounds)
})
ipcMain.on('window-set-always-on-top', (event, flag) => {
app.window.setAlwaysOnTop(flag)
})
ipcMain.on('window-set-vibrancy', (event, enabled) => {
setWindowVibrancy(enabled)
})
}
const setupMenu = () => {
let template = [{
label: 'Application',
submenu: [
{ role: 'about', label: 'About Terminus' },
{ type: 'separator' },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click () {
app.window.webContents.send('host:preferences-menu')
},
},
{ type: 'separator' },
{ role: 'services', submenu: [] },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{
label: 'Quit',
accelerator: 'Cmd+Q',
click () {
app.quit()
},
},
],
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' },
],
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forcereload' },
{ role: 'toggledevtools' },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
{
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
{ type: 'separator' },
{ role: 'front' },
],
},
{
role: 'help',
submenu: [
{
label: 'Website',
click () {
shell.openExternal('https://eugeny.github.io/terminus')
},
},
],
}]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}
const start = () => {
let t0 = Date.now()
let configPath = path.join(app.getPath('userData'), 'config.yaml')
let configData
if (fs.existsSync(configPath)) {
configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'))
} else {
configData = {}
}
let options = {
width: 800,
height: 600,
title: 'Terminus',
minWidth: 400,
minHeight: 300,
webPreferences: { webSecurity: false },
frame: false,
show: false,
}
Object.assign(options, windowConfig.get('windowBoundaries'))
if ((configData.appearance || {}).frame === 'native') {
options.frame = true
} else {
if (process.platform === 'darwin') {
options.titleBarStyle = 'hiddenInset'
}
}
if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
options.transparent = true
}
if (process.platform === 'linux') {
options.backgroundColor = '#131d27'
}
app.commandLine.appendSwitch('disable-http-cache')
app.window = new BrowserWindow(options)
app.window.once('ready-to-show', () => {
if (process.platform === 'darwin') {
app.window.setVibrancy('dark')
} else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
setWindowVibrancy(true)
}
app.window.show()
app.window.focus()
})
app.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, { extraHeaders: 'pragma: no-cache\n' })
if (process.platform !== 'darwin') {
app.window.setMenu(null)
}
setupWindowManagement()
if (process.platform === 'darwin') {
setupMenu()
} else {
app.window.setMenu(null)
}
console.info(`Host startup: ${Date.now() - t0}ms`)
t0 = Date.now()
ipcMain.on('app:ready', () => {
console.info(`App startup: ${Date.now() - t0}ms`)
})
}
app.on('activate', () => {
if (!app.window) {
start()
} else {
app.window.show()
app.window.focus()
}
})
process.on('uncaughtException', function (err) {
console.log(err)
app.window.webContents.send('uncaughtException', err)
})
app.on('second-instance', (event, argv, cwd) => {
app.window.webContents.send('host:second-instance', parseArgs(argv, cwd))
})
const argv = parseArgs(process.argv, process.cwd())
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
if (argv.d) {
electronDebug({ enabled: true, showDevTools: 'undocked' })
}
app.on('ready', start)

15
app/lib/lru.js Normal file
View File

@@ -0,0 +1,15 @@
let lru = require('lru-cache')({ max: 256, maxAge: 250 })
let fs = require('fs')
let origLstat = fs.realpathSync.bind(fs)
// NB: The biggest offender of thrashing realpathSync is the node module system
// itself, which we can't get into via any sane means.
require('fs').realpathSync = function (p) {
let r = lru.get(p)
if (r) return r
r = origLstat(p)
lru.set(p, r)
return r
}

View File

@@ -1,15 +0,0 @@
var lru = require('lru-cache')({max: 256, maxAge: 250/*ms*/});
var fs = require('fs');
var origLstat = fs.realpathSync.bind(fs);
console.log('s')
// NB: The biggest offender of thrashing realpathSync is the node module system
// itself, which we can't get into via any sane means.
require('fs').realpathSync = function(p) {
let r = lru.get(p);
if (r) return r;
r = origLstat(p);
lru.set(p, r);
return r;
};

View File

@@ -1,317 +0,0 @@
require('./lru.js')
if (process.platform == 'win32' && require('electron-squirrel-startup')) process.exit(0)
const electron = require('electron')
let electronVibrancy
if (process.platform != 'linux') {
electronVibrancy = require('electron-vibrancy')
}
let app = electron.app
const yaml = require('js-yaml')
const path = require('path')
const fs = require('fs')
const Config = require('electron-config')
let windowConfig = new Config({name: 'window'})
if (!process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS = ''
}
setWindowVibrancy = (enabled) => {
if (enabled && !app.window.vibrancyViewID) {
app.window.vibrancyViewID = electronVibrancy.SetVibrancy(app.window, 0)
} else if (!enabled && app.window.vibrancyViewID) {
electronVibrancy.RemoveView(app.window, app.window.vibrancyViewID)
app.window.vibrancyViewID = null
}
}
setupWindowManagement = () => {
app.window.on('show', () => {
app.window.webContents.send('host:window-shown')
if (app.tray) {
app.tray.destroy()
app.tray = null
}
})
app.window.on('hide', (e) => {
if (!app.tray) {
setupTray()
}
})
app.window.on('enter-full-screen', () => app.window.webContents.send('host:window-enter-full-screen'))
app.window.on('leave-full-screen', () => app.window.webContents.send('host:window-leave-full-screen'))
app.window.on('close', (e) => {
windowConfig.set('windowBoundaries', app.window.getBounds())
})
app.window.on('closed', () => {
app.window = null
})
electron.ipcMain.on('window-focus', () => {
app.window.focus()
})
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()) {
app.window.unmaximize()
} else {
app.window.maximize()
}
})
electron.ipcMain.on('window-minimize', () => {
app.window.minimize()
})
electron.ipcMain.on('window-set-bounds', (event, bounds) => {
app.window.setBounds(bounds)
})
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
app.window.setAlwaysOnTop(flag)
})
electron.ipcMain.on('window-set-vibrancy', (event, enabled) => {
setWindowVibrancy(enabled)
})
}
setupTray = () => {
if (process.platform == 'darwin') {
app.tray = new electron.Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`)
app.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`)
} else {
app.tray = new electron.Tray(`${app.getAppPath()}/assets/tray.png`)
}
app.tray.on('click', () => {
app.window.show()
app.window.focus()
})
const contextMenu = electron.Menu.buildFromTemplate([{
label: 'Show',
click () {
app.window.show()
app.window.focus()
}
}])
if (process.platform != 'darwin') {
app.tray.setContextMenu(contextMenu)
}
app.tray.setToolTip(`Terminus ${app.getVersion()}`)
}
setupMenu = () => {
let template = [{
label: "Application",
submenu: [
{ role: 'about', label: 'About Terminus' },
{ type: 'separator' },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click () {
app.window.webContents.send('host:preferences-menu')
}
},
{ type: 'separator' },
{ role: 'services', submenu: [] },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{
label: 'Quit',
accelerator: 'Cmd+Q',
click () {
app.quit()
}
}
]
},
{
label: "Edit",
submenu: [
{role: 'undo'},
{role: 'redo'},
{type: 'separator'},
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{role: 'pasteandmatchstyle'},
{role: 'delete'},
{role: 'selectall'}
]
},
{
label: 'View',
submenu: [
{role: 'reload'},
{role: 'forcereload'},
{role: 'toggledevtools'},
{type: 'separator'},
{role: 'resetzoom'},
{role: 'zoomin'},
{role: 'zoomout'},
{type: 'separator'},
{role: 'togglefullscreen'}
]
},
{
role: 'window',
submenu: [
{role: 'minimize'},
{role: 'zoom'},
{type: 'separator'},
{role: 'front'}
]
},
{
role: 'help',
submenu: [
{
label: 'Website',
click () { electron.shell.openExternal('https://eugeny.github.io/terminus') }
}
]
}]
electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate(template))
}
start = () => {
let t0 = Date.now()
let configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
let configData
if (fs.existsSync(configPath)) {
configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'))
} else {
configData = {}
}
let options = {
width: 800,
height: 600,
title: 'Terminus',
minWidth: 400,
minHeight: 300,
webPreferences: {webSecurity: false},
frame: false,
show: false,
}
Object.assign(options, windowConfig.get('windowBoundaries'))
if ((configData.appearance || {}).frame == 'native') {
options.frame = true
} else {
if (process.platform == 'darwin') {
options.titleBarStyle = 'hiddenInset'
}
}
if (process.platform == 'win32' && (configData.appearance || {}).vibrancy) {
options.transparent = true
}
if (process.platform == 'linux') {
options.backgroundColor = '#131d27'
}
app.commandLine.appendSwitch('disable-http-cache')
app.window = new electron.BrowserWindow(options)
app.window.once('ready-to-show', () => {
if (process.platform == 'darwin') {
app.window.setVibrancy('dark')
} else if (process.platform == 'win32' && (configData.appearance || {}).vibrancy) {
setWindowVibrancy(true)
}
app.window.show()
app.window.focus()
})
app.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, {extraHeaders: "pragma: no-cache\n"})
if (process.platform != 'darwin') {
app.window.setMenu(null)
}
setupWindowManagement()
if (process.platform == 'darwin') {
setupMenu()
} else {
app.window.setMenu(null)
}
console.info(`Host startup: ${Date.now() - t0}ms`)
t0 = Date.now()
electron.ipcMain.on('app:ready', () => {
console.info(`App startup: ${Date.now() - t0}ms`)
})
}
app.on('activate', () => {
if (!app.window)
start()
else {
app.window.show()
app.window.focus()
}
})
process.on('uncaughtException', function(err) {
console.log(err)
app.window.webContents.send('uncaughtException', err)
})
const argv = require('yargs')
.usage('terminus [command] [arguments]')
.version('v', 'Show version and exit', app.getVersion())
.alias('d', 'debug')
.describe('d', 'Show DevTools on start')
.alias('h', 'help')
.help('h')
.strict()
.argv
app.on('second-instance', (argv, cwd) => {
app.window.webContents.send('host:second-instance', argv, cwd)
})
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
if (argv.d) {
require('electron-debug')({enabled: true, showDevTools: 'undocked'})
}
app.on('ready', start)

View File

@@ -5,7 +5,7 @@
"name": "Eugene Pankov",
"email": "e@ajenti.org"
},
"main": "main.js",
"main": "dist/main.js",
"version": "1.0.0-alpha.1",
"scripts": {
"build": "webpack --progress --color --display-modules",
@@ -22,7 +22,7 @@
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
"devtron": "1.4.0",
"electron-config": "0.2.1",
"electron-debug": "^1.0.1",
"electron-debug": "^2.0.0",
"electron-is-dev": "0.1.2",
"electron-squirrel-startup": "^1.0.0",
"electron-vibrancy": "^0.1.3",

View File

@@ -1,4 +1,4 @@
import '../lru.js'
import '../lib/lru.js'
import 'source-sans-pro'
import 'font-awesome/css/font-awesome.css'
import 'ngx-toastr/toastr.css'

View File

@@ -17,7 +17,7 @@
"lib": [
"dom",
"es2015",
"es2015.iterable.ts",
"es2015.iterable",
"es2017",
"es7"
]

View File

@@ -0,0 +1,49 @@
const path = require('path')
const webpack = require('webpack')
module.exports = {
name: 'terminus-main',
target: 'node',
entry: {
main: path.resolve(__dirname, 'lib/index.js'),
},
mode: process.env.DEV ? 'development' : 'production',
context: __dirname,
devtool: 'source-map',
output: {
path: path.join(__dirname, 'dist'),
pathinfo: true,
filename: '[name].js',
},
resolve: {
modules: ['lib/', 'node_modules', '../node_modules'].map(x => path.join(__dirname, x)),
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /lib[\\/].*\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['babel-preset-es2015'],
},
},
},
],
},
externals: {
electron: 'commonjs electron',
'electron-config': 'commonjs electron-config',
'electron-vibrancy': 'commonjs electron-vibrancy',
'electron-squirrel-startup': 'commonjs electron-squirrel-startup',
fs: 'commonjs fs',
mz: 'commonjs mz',
path: 'commonjs path',
yargs: 'commonjs yargs',
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
],
}

View File

@@ -149,27 +149,33 @@ electron-config@0.2.1:
dependencies:
conf "^0.11.1"
electron-debug@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-1.2.0.tgz#22e51a73e1bf095d0bb51a6c3d97a203364c4222"
electron-debug@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-2.0.0.tgz#3059a6557acbfb091f138d83875f57bac80cea6d"
dependencies:
electron-is-dev "^0.1.0"
electron-localshortcut "^2.0.0"
electron-is-dev "^0.3.0"
electron-localshortcut "^3.0.0"
electron-is-accelerator@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
electron-is-dev@0.1.2, electron-is-dev@^0.1.0:
electron-is-dev@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.1.2.tgz#8a1043e32b3a1da1c3f553dce28ce764246167e3"
electron-localshortcut@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-2.0.2.tgz#6a1adcd6514c957328ec7912f5ccb5e1c10706db"
electron-is-dev@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
electron-localshortcut@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz#10c1ffd537b8d39170aaf6e1551341f7780dd2ce"
dependencies:
debug "^2.6.8"
electron-is-accelerator "^0.1.0"
keyboardevent-from-electron-accelerator "^1.1.0"
keyboardevents-areequal "^0.2.1"
electron-squirrel-startup@^1.0.0:
version "1.0.0"
@@ -270,6 +276,14 @@ js-yaml@3.8.2:
argparse "^1.0.7"
esprima "^3.1.1"
keyboardevent-from-electron-accelerator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.0.tgz#324614f6e33490c37ffc5be5876b3e85fe223c84"
keyboardevents-areequal@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"