diff --git a/README.md b/README.md
index 4c789169..ae43fe03 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,16 @@
-# Terminus α
-*A terminal for a more modern age*
+
+

+
Terminus α
+
+ A terminal for a more modern age
+
+
+
+
+
[](https://travis-ci.org/Eugeny/terminus) [](https://ci.appveyor.com/project/Eugeny/terminus) [](https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE) [](https://github.com/Eugeny/terminus/releases/latest)
+[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_shield)
----
@@ -13,12 +22,12 @@
* Theming and color schemes
* Configurable hotkey schemes
* **GNU Screen** style hotkeys available by default
- * Default Linux style hotkeys for Copy(`Ctrl`+`Shift`+`C`), and Paste(`Ctrl`+`Shift`+`V`)
* Full Unicode support including double-width characters
* Doesn't choke on fast-flowing outputs
* Tab persistence on macOS and Linux
* Proper shell-like experience on Windows including tab completion (thanks, Clink!)
* CMD, PowerShell, Cygwin, Git-Bash and Bash on Windows support
+ * Default Linux style hotkeys for copy (`Ctrl`+`Shift`+`C`) and paste (`Ctrl`+`Shift`+`V`)
---
@@ -28,6 +37,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
* [theme-hype](https://github.com/Eugeny/terminus-theme-hype) - a Hyper inspired theme
+ * [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
---
@@ -36,3 +46,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
Pull requests and plugins are welcome! Publish your plugin on NPM with a `terminus-plugin` keyword to make them appear in the Plugin Manager.
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) for a very brief plugin development tutorial!
+
+
+## License
+[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_large)
\ No newline at end of file
diff --git a/app/index.pug b/app/index.pug
index 1132e12c..82238d51 100644
--- a/app/index.pug
+++ b/app/index.pug
@@ -13,22 +13,9 @@ html
app-root
.preload-logo
div
- .terminus-logo.animated
- .part(style='transform: rotateZ(0deg)')
- div
- .part(style='transform: rotateZ(51deg)')
- div
- .part(style='transform: rotateZ(102deg)')
- div
- .part(style='transform: rotateZ(154deg)')
- div
- .part(style='transform: rotateZ(205deg)')
- div
- .part(style='transform: rotateZ(257deg)')
- div
- .part(style='transform: rotateZ(308deg)')
- div
+ .terminus-logo
h1.terminus-title Terminus
+ sup α
.progress
.bar(style='width: 0%')
diff --git a/app/main.js b/app/main.js
index 09e5295a..5814d998 100644
--- a/app/main.js
+++ b/app/main.js
@@ -30,28 +30,18 @@ if (!process.env.TERMINUS_PLUGINS) {
}
setupWindowManagement = () => {
- let windowCloseable
-
app.window.on('show', () => {
app.window.webContents.send('host:window-shown')
})
app.window.on('close', (e) => {
windowConfig.set('windowBoundaries', app.window.getBounds())
- if (!windowCloseable) {
- app.window.minimize()
- e.preventDefault()
- }
})
app.window.on('closed', () => {
app.window = null
})
- electron.ipcMain.on('window-closeable', (event, flag) => {
- windowCloseable = flag
- })
-
electron.ipcMain.on('window-focus', () => {
app.window.focus()
})
@@ -86,6 +76,8 @@ setupWindowManagement = () => {
electron.ipcMain.on('window-set-bounds', (event, bounds) => {
let actualBounds = app.window.getBounds()
+ actualBounds.width -= bounds.x - actualBounds.x
+ actualBounds.height -= bounds.y - actualBounds.y
actualBounds.x = bounds.x
actualBounds.y = bounds.y
app.window.setBounds(actualBounds)
@@ -102,8 +94,6 @@ setupWindowManagement = () => {
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
app.window.setAlwaysOnTop(flag)
})
-
- app.on('before-quit', () => windowCloseable = true)
}
@@ -131,7 +121,7 @@ setupMenu = () => {
label: 'Quit',
accelerator: 'Cmd+Q',
click () {
- app.window.webContents.send('host:quit-request')
+ app.quit()
}
}
]
@@ -202,7 +192,6 @@ start = () => {
let options = {
width: 800,
height: 600,
- //icon: `${app.getAppPath()}/assets/img/icon.png`,
title: 'Terminus',
minWidth: 400,
minHeight: 300,
diff --git a/app/package.json b/app/package.json
index fe991d5e..179e1e1a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -12,14 +12,14 @@
"watch": "webpack --progress --color --watch"
},
"dependencies": {
- "@angular/animations": "4.0.1",
- "@angular/common": "4.0.1",
- "@angular/compiler": "4.0.1",
- "@angular/core": "4.0.1",
- "@angular/forms": "4.0.1",
- "@angular/platform-browser": "4.0.1",
- "@angular/platform-browser-dynamic": "4.0.1",
- "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
+ "@angular/animations": "4.3.0",
+ "@angular/common": "4.3.0",
+ "@angular/compiler": "4.3.0",
+ "@angular/core": "4.3.0",
+ "@angular/forms": "4.3.0",
+ "@angular/platform-browser": "4.3.0",
+ "@angular/platform-browser-dynamic": "4.3.0",
+ "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.28",
"devtron": "1.4.0",
"electron-config": "0.2.1",
"electron-debug": "^1.0.1",
@@ -29,7 +29,7 @@
"mz": "^2.6.0",
"path": "0.12.7",
"rxjs": "5.3.0",
- "zone.js": "0.8.4"
+ "zone.js": "0.8.12"
},
"devDependencies": {
"@types/mz": "0.0.31"
diff --git a/app/src/app.module.ts b/app/src/app.module.ts
index 6df9e91d..35f691fe 100644
--- a/app/src/app.module.ts
+++ b/app/src/app.module.ts
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
-export async function getRootModule (plugins: any[]): Promise {
+export function getRootModule (plugins: any[]) {
let imports = [
BrowserModule,
...(plugins.map(x => x.default.forRoot ? x.default.forRoot() : x.default)),
diff --git a/app/src/entry.preload.ts b/app/src/entry.preload.ts
index 8478ea2f..9a2987a3 100644
--- a/app/src/entry.preload.ts
+++ b/app/src/entry.preload.ts
@@ -31,3 +31,7 @@ process.on('uncaughtException', (err) => {
Raven.captureException(err)
console.error(err)
})
+
+const childProcess = require('child_process')
+childProcess.spawn = require('electron').remote.require('child_process').spawn
+childProcess.exec = require('electron').remote.require('child_process').exec
diff --git a/app/src/entry.ts b/app/src/entry.ts
index a564f756..5f7e6c40 100644
--- a/app/src/entry.ts
+++ b/app/src/entry.ts
@@ -6,13 +6,13 @@ import 'rxjs'
// Always land on the start view
location.hash = ''
-import { enableProdMode } from '@angular/core'
+import { enableProdMode, NgModuleRef } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { getRootModule } from './app.module'
-import { findPlugins, loadPlugins } from './plugins'
+import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
-if (process.platform == 'win32') {
+if (process.platform === 'win32') {
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
}
@@ -22,10 +22,29 @@ if (require('electron-is-dev')) {
enableProdMode()
}
-findPlugins().then(async plugins => {
+async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise> {
+ if (safeMode) {
+ plugins = plugins.filter(x => x.isBuiltin)
+ }
let pluginsModules = await loadPlugins(plugins, (current, total) => {
(document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%'
})
- let module = await getRootModule(pluginsModules)
- platformBrowserDynamic().bootstrapModule(module)
+ let module = getRootModule(pluginsModules)
+ return await platformBrowserDynamic().bootstrapModule(module)
+}
+
+findPlugins().then(async plugins => {
+ console.log('Starting with plugins:', plugins)
+ try {
+ await bootstrap(plugins)
+ } catch (error) {
+ console.error('Angular bootstrapping error:', error)
+ console.warn('Trying safe mode')
+ window['safeModeReason'] = error
+ try {
+ await bootstrap(plugins, true)
+ } catch (error) {
+ console.error('Bootstrap failed:', error)
+ }
+ }
})
diff --git a/app/src/logo.svg b/app/src/logo.svg
new file mode 100644
index 00000000..ca74f3ab
--- /dev/null
+++ b/app/src/logo.svg
@@ -0,0 +1,89 @@
+
+
+
+
diff --git a/app/src/plugins.ts b/app/src/plugins.ts
index 60072267..21a4200f 100644
--- a/app/src/plugins.ts
+++ b/app/src/plugins.ts
@@ -20,7 +20,7 @@ if (process.env.DEV) {
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
}
-const builtinPluginsPath = path.join((process as any).resourcesPath, 'builtin-plugins')
+const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const userPluginsPath = path.join(
require('electron').remote.app.getPath('appData'),
@@ -108,8 +108,9 @@ export async function findPlugins (): Promise {
continue
}
- if (foundPlugins.some(x => x.name === pluginName)) {
- console.info(`Plugin ${pluginName} already exists`)
+ if (foundPlugins.some(x => x.name === pluginName.substring('terminus-'.length))) {
+ console.info(`Plugin ${pluginName} already exists, overriding`)
+ foundPlugins = foundPlugins.filter(x => x.name !== pluginName.substring('terminus-'.length))
}
try {
diff --git a/app/src/preload.scss b/app/src/preload.scss
index c5a1d0be..95742e57 100644
--- a/app/src/preload.scss
+++ b/app/src/preload.scss
@@ -1,6 +1,3 @@
-$color: rgba(66, 142, 173, 0.75);
-
-
.preload-logo {
-webkit-app-region: drag;
position: fixed;
@@ -24,7 +21,7 @@ $color: rgba(66, 142, 173, 0.75);
.bar {
transition: 1s ease-out width;
- background: $color;
+ background: #a1c5e4;
height: 3px;
}
}
@@ -42,63 +39,22 @@ $color: rgba(66, 142, 173, 0.75);
.terminus-logo {
width: 160px;
height: 160px;
+ background: url('./logo.svg');
+ background-repeat: none;
+ background-size: contain;
margin: auto;
- position: relative;
- transform: rotateZ(-14.5deg);
-
- .part {
- position: absolute;
- width: 160px;
- height: 160px;
-
- div {
- position: absolute;
- top: 33px;
- left: 24px;
- width: 44px;
- height: 44px;
- background: $color;
- transform: rotateX(52deg) rotateY(-42deg);
- animation: terminusLogoPartOnce ease-out 1s;
- }
- }
-
- &.animated .part div {
- animation: terminusLogoPart infinite ease-out 2s;
- }
}
.terminus-title {
- color: $color;
+ color: #a1c5e4;
font-family: 'Source Sans Pro';
text-align: center;
font-weight: normal;
font-size: 42px;
margin: 0;
-}
-
-@keyframes terminusLogoPart {
- 0% {
- transform: rotateX(90deg) rotateY(-90deg);
- }
- 25% {
- transform: rotateX(52deg) rotateY(-42deg);
- }
- 75% {
- transform: rotateX(52deg) rotateY(-42deg);
- }
- 100% {
- transform: rotateX(-90deg) rotateY(-90deg);
- }
-}
-
-@keyframes terminusLogoPartOnce {
- 0% {
- transform: rotateX(90deg) rotateY(-90deg);
- }
- 100% {
- transform: rotateX(52deg) rotateY(-42deg);
+ sup {
+ color: #842fe0;
}
}
diff --git a/app/webpack.config.js b/app/webpack.config.js
index 0c44a674..8fccad2a 100644
--- a/app/webpack.config.js
+++ b/app/webpack.config.js
@@ -55,6 +55,7 @@ module.exports = {
'@angular/forms': 'commonjs @angular/forms',
'@angular/common': 'commonjs @angular/common',
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
+ 'child_process': 'commonjs child_process',
'electron': 'commonjs electron',
'electron-is-dev': 'commonjs electron-is-dev',
'module': 'commonjs module',
diff --git a/app/yarn.lock b/app/yarn.lock
index 3da0d71c..b1c274f8 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -2,37 +2,51 @@
# yarn lockfile v1
-"@angular/animations@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.0.1.tgz#154420c8ee5c22fbaf1434b6d156150cf5218da6"
+"@angular/animations@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.3.0.tgz#56f34b84649379202ac359929b82eb0b915e9c72"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/common@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.0.1.tgz#df488eada842b2d841ded750712292b18387b5b0"
+"@angular/common@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.3.0.tgz#13a54a6929dd52f9729b16ae446fad58fe163053"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/compiler@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.0.1.tgz#15721edb148167a2d83b6f9324817e658eac8280"
+"@angular/compiler@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.3.0.tgz#55503bf27a1f062f71b9495393f3311903a8fc43"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/core@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.0.1.tgz#0b110a001012076ea696460ccd922707bcdf51ba"
+"@angular/core@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.3.0.tgz#bd2249c3de1224a7c6536c4aba728d6565329334"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/forms@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.0.1.tgz#b9ebdbbb8ace0f9a3bf9e53c299eafdfab1d5041"
+"@angular/forms@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.3.0.tgz#7d0c7a854737e9a30a5fd9665f8d4f56a1b91bd8"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/platform-browser-dynamic@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.0.1.tgz#fd5debb2d3f6474350965e71c2674e2170d7cfcb"
+"@angular/platform-browser-dynamic@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.3.0.tgz#551fb18851b27ee8f3e4b0ee25aad10bd7b312e3"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/platform-browser@4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.0.1.tgz#4b9efbeb2fbb900de188743b988802d3aa2b33ff"
+"@angular/platform-browser@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.3.0.tgz#02389489185185c3becf06359346100e5479c7e1"
+ dependencies:
+ tslib "^1.7.1"
-"@ng-bootstrap/ng-bootstrap@1.0.0-alpha.22":
- version "1.0.0-alpha.22"
- resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.22.tgz#aaad058cc39293ea6184e4b9b849f298c0b11a86"
+"@ng-bootstrap/ng-bootstrap@^1.0.0-alpha.28":
+ version "1.0.0-alpha.28"
+ resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.28.tgz#30a6503bf7f94f9d3187591fb3267b59cc0cdaad"
"@types/mz@0.0.31":
version "0.0.31"
@@ -41,8 +55,8 @@
"@types/node" "*"
"@types/node@*":
- version "8.0.7"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.7.tgz#fb0ad04b5b6f6eabe0372a32a8f1fbba5c130cae"
+ version "8.0.13"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
accessibility-developer-tools@^2.11.0:
version "2.12.0"
@@ -244,12 +258,16 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
+tslib@^1.7.1:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec"
+
util@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies:
inherits "2.0.1"
-zone.js@0.8.4:
- version "0.8.4"
- resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.4.tgz#cc40ae5a1c879601c5ebba2096b5c80f0c4c3602"
+zone.js@0.8.12:
+ version "0.8.12"
+ resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb"
diff --git a/build/icons/128x128.png b/build/icons/128x128.png
index 5776baf9..9e9f87a4 100644
Binary files a/build/icons/128x128.png and b/build/icons/128x128.png differ
diff --git a/build/icons/16x16.png b/build/icons/16x16.png
index e630b2a4..497c87ac 100644
Binary files a/build/icons/16x16.png and b/build/icons/16x16.png differ
diff --git a/build/icons/256x256.png b/build/icons/256x256.png
index f9eb7a65..909b2fd2 100644
Binary files a/build/icons/256x256.png and b/build/icons/256x256.png differ
diff --git a/build/icons/32x32.png b/build/icons/32x32.png
index ee79ae12..713bdee6 100644
Binary files a/build/icons/32x32.png and b/build/icons/32x32.png differ
diff --git a/build/icons/512x512.png b/build/icons/512x512.png
index 26c650b8..3fb2fcd5 100644
Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ
diff --git a/build/icons/64x64.png b/build/icons/64x64.png
index bc990f65..2efed529 100644
Binary files a/build/icons/64x64.png and b/build/icons/64x64.png differ
diff --git a/build/icons/icon.svg b/build/icons/icon.svg
new file mode 100644
index 00000000..d1b5bc68
--- /dev/null
+++ b/build/icons/icon.svg
@@ -0,0 +1,118 @@
+
+
+
+
diff --git a/build/mac/icon.icns b/build/mac/icon.icns
index fa396ca1..714ac05e 100644
Binary files a/build/mac/icon.icns and b/build/mac/icon.icns differ
diff --git a/build/windows/icon.ico b/build/windows/icon.ico
index 2f63a466..a7ec161a 100644
Binary files a/build/windows/icon.ico and b/build/windows/icon.ico differ
diff --git a/docs/linux.png b/docs/linux.png
index 59568a0a..30d04f9f 100644
Binary files a/docs/linux.png and b/docs/linux.png differ
diff --git a/package.json b/package.json
index d26aab90..77134e4c 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,8 @@
"libnotify4",
"libappindicator1",
"libxtst6",
- "libnss3"
+ "libnss3",
+ "tmux"
]
},
"rpm": {
diff --git a/scripts/install-deps.js b/scripts/install-deps.js
index 8291250b..59351bbe 100755
--- a/scripts/install-deps.js
+++ b/scripts/install-deps.js
@@ -5,21 +5,18 @@ const vars = require('./vars')
const log = require('npmlog')
log.info('deps', 'app')
-sh.exec('npm prune')
-sh.exec('npm install')
-sh.exec('npm update --dev')
+sh.exec('yarn prune')
+sh.exec('yarn install')
sh.cd('app')
-sh.exec('npm prune')
-sh.exec('npm install')
-sh.exec('npm update --dev')
+sh.exec('yarn prune')
+sh.exec('yarn install')
sh.cd('..')
vars.builtinPlugins.forEach(plugin => {
log.info('deps', plugin)
sh.cd(plugin)
- sh.exec('npm prune')
- sh.exec('npm install')
- sh.exec('npm update --dev')
+ sh.exec('yarn prune')
+ sh.exec('yarn install')
sh.cd('..')
})
diff --git a/terminus-community-color-schemes/package.json b/terminus-community-color-schemes/package.json
index 50cf28bf..0ca2a5d3 100644
--- a/terminus-community-color-schemes/package.json
+++ b/terminus-community-color-schemes/package.json
@@ -1,6 +1,6 @@
{
"name": "terminus-community-color-schemes",
- "version": "1.0.0-alpha.16-8-gfc060ac",
+ "version": "1.0.0-alpha.24",
"description": "Community color schemes for Terminus",
"keywords": [
"terminus-plugin"
diff --git a/terminus-core/package.json b/terminus-core/package.json
index 36c1d903..bec38558 100644
--- a/terminus-core/package.json
+++ b/terminus-core/package.json
@@ -1,6 +1,6 @@
{
"name": "terminus-core",
- "version": "1.0.0-alpha.16-8-gfc060ac",
+ "version": "1.0.0-alpha.24",
"description": "Terminus core",
"keywords": [
"terminus-plugin"
@@ -17,14 +17,13 @@
"author": "Eugene Pankov",
"license": "MIT",
"devDependencies": {
- "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
- "@types/js-yaml": "^3.5.29",
- "@types/node": "^7.0.12",
+ "@types/js-yaml": "^3.9.0",
+ "@types/node": "^7.0.37",
"@types/webpack-env": "^1.13.0",
"bootstrap": "4.0.0-alpha.6",
"core-js": "^2.4.1",
"ngx-perfect-scrollbar": "4.0.0",
- "typescript": "^2.4.0"
+ "typescript": "^2.4.1"
},
"peerDependencies": {
"@angular/animations": "4.0.1",
@@ -37,8 +36,8 @@
"zone.js": "0.8.4"
},
"dependencies": {
- "deepmerge": "^1.4.4",
- "js-yaml": "^3.8.4"
+ "deepmerge": "^1.5.0",
+ "js-yaml": "^3.9.0"
},
"false": {}
}
diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts
index 195ab8f5..fb59cd26 100644
--- a/terminus-core/src/api/index.ts
+++ b/terminus-core/src/api/index.ts
@@ -1,5 +1,5 @@
export { BaseTabComponent } from '../components/baseTab.component'
-export { TabRecoveryProvider } from './tabRecovery'
+export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider'
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
diff --git a/terminus-core/src/api/tabRecovery.ts b/terminus-core/src/api/tabRecovery.ts
index d5f23220..f3473e77 100644
--- a/terminus-core/src/api/tabRecovery.ts
+++ b/terminus-core/src/api/tabRecovery.ts
@@ -1,3 +1,10 @@
-export abstract class TabRecoveryProvider {
- abstract async recover (recoveryToken: any): Promise
+import { TabComponentType } from '../services/app.service'
+
+export interface RecoveredTab {
+ type: TabComponentType,
+ options?: any,
+}
+
+export abstract class TabRecoveryProvider {
+ abstract async recover (recoveryToken: any): Promise
}
diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug
index c9832dc3..6a63790f 100644
--- a/terminus-core/src/components/appRoot.component.pug
+++ b/terminus-core/src/components/appRoot.component.pug
@@ -19,7 +19,7 @@ title-bar(
[class.drag-region]='hostApp.platform == Platform.macOS',
@animateTab,
(click)='app.selectTab(tab)',
- (closeClicked)='app.closeTab(tab)',
+ (closeClicked)='app.closeTab(tab, true)',
)
.btn-group
diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts
index d57e51db..30940085 100644
--- a/terminus-core/src/components/appRoot.component.ts
+++ b/terminus-core/src/components/appRoot.component.ts
@@ -1,16 +1,17 @@
import { Component, Inject, Input, HostListener } from '@angular/core'
import { trigger, style, animate, transition, state } from '@angular/animations'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ElectronService } from '../services/electron.service'
import { HostAppService, Platform } from '../services/hostApp.service'
import { HotkeysService } from '../services/hotkeys.service'
import { Logger, LogService } from '../services/log.service'
-import { QuitterService } from '../services/quitter.service'
import { ConfigService } from '../services/config.service'
import { DockingService } from '../services/docking.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
import { ThemesService } from '../services/themes.service'
+import { SafeModeModalComponent } from './safeModeModal.component'
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
@Component({
@@ -28,9 +29,16 @@ import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
'flex-basis': '1px',
'width': '1px',
}),
- animate('250ms ease-in-out')
+ animate('250ms ease-in-out', style({
+ 'flex-basis': '200px',
+ 'width': '200px',
+ }))
]),
transition(':leave', [
+ style({
+ 'flex-basis': '200px',
+ 'width': '200px',
+ }),
animate('250ms ease-in-out', style({
'flex-basis': '1px',
'width': '1px',
@@ -56,8 +64,8 @@ export class AppRootComponent {
public app: AppService,
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
log: LogService,
+ ngbModal: NgbModal,
_themes: ThemesService,
- _quitter: QuitterService,
) {
this.logger = log.create('main')
this.logger.info('v', electron.app.getVersion())
@@ -74,7 +82,7 @@ export class AppRootComponent {
}
if (this.app.activeTab) {
if (hotkey === 'close-tab') {
- this.app.closeTab(this.app.activeTab)
+ this.app.closeTab(this.app.activeTab, true)
}
if (hotkey === 'toggle-last-tab') {
this.app.toggleLastTab()
@@ -99,6 +107,10 @@ export class AppRootComponent {
this.hotkeys.globalHotkey.subscribe(() => {
this.onGlobalHotkey()
})
+
+ if (window['safeModeReason']) {
+ ngbModal.open(SafeModeModalComponent)
+ }
}
onGlobalHotkey () {
@@ -133,16 +145,6 @@ export class AppRootComponent {
}
}
- private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
- let buttons: IToolbarButton[] = []
- this.toolbarButtonProviders.forEach((provider) => {
- buttons = buttons.concat(provider.provide())
- })
- return buttons
- .filter((button) => (button.weight > 0) === aboveZero)
- .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
- }
-
@HostListener('dragover')
onDragOver () {
return false
@@ -152,4 +154,14 @@ export class AppRootComponent {
onDrop () {
return false
}
+
+ private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
+ let buttons: IToolbarButton[] = []
+ this.toolbarButtonProviders.forEach((provider) => {
+ buttons = buttons.concat(provider.provide())
+ })
+ return buttons
+ .filter((button) => (button.weight > 0) === aboveZero)
+ .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
+ }
}
diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts
index 1e24a7d1..77a15538 100644
--- a/terminus-core/src/components/baseTab.component.ts
+++ b/terminus-core/src/components/baseTab.component.ts
@@ -1,10 +1,11 @@
-import { Subject, BehaviorSubject } from 'rxjs'
+import { Subject } from 'rxjs'
import { ViewRef } from '@angular/core'
export abstract class BaseTabComponent {
private static lastTabID = 0
id: number
- title$ = new BehaviorSubject(null)
+ title: string
+ customTitle: string
scrollable: boolean
hasActivity = false
focused$ = new Subject()
@@ -30,9 +31,12 @@ export abstract class BaseTabComponent {
return null
}
+ async canClose (): Promise {
+ return true
+ }
+
destroy (): void {
this.focused$.complete()
this.blurred$.complete()
- this.title$.complete()
}
}
diff --git a/terminus-core/src/components/renameTabModal.component.pug b/terminus-core/src/components/renameTabModal.component.pug
new file mode 100644
index 00000000..901ceb22
--- /dev/null
+++ b/terminus-core/src/components/renameTabModal.component.pug
@@ -0,0 +1,6 @@
+.modal-body
+ input.form-control(type='text', [(ngModel)]='value', (keyup.enter)='save()', autofocus)
+
+.modal-footer
+ button.btn.btn-outline-primary((click)='save()') Save
+ button.btn.btn-outline-secondary((click)='close()') Cancel
diff --git a/terminus-core/src/components/renameTabModal.component.ts b/terminus-core/src/components/renameTabModal.component.ts
new file mode 100644
index 00000000..bbca5daa
--- /dev/null
+++ b/terminus-core/src/components/renameTabModal.component.ts
@@ -0,0 +1,22 @@
+import { Component, Input } from '@angular/core'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+
+@Component({
+ selector: 'rename-tab-modal',
+ template: require('./renameTabModal.component.pug'),
+})
+export class RenameTabModalComponent {
+ @Input() value: string
+
+ constructor (
+ private modalInstance: NgbActiveModal
+ ) { }
+
+ save () {
+ this.modalInstance.close(this.value)
+ }
+
+ close () {
+ this.modalInstance.dismiss()
+ }
+}
diff --git a/terminus-core/src/components/safeModeModal.component.pug b/terminus-core/src/components/safeModeModal.component.pug
new file mode 100644
index 00000000..5d55cc7c
--- /dev/null
+++ b/terminus-core/src/components/safeModeModal.component.pug
@@ -0,0 +1,7 @@
+.modal-body
+ .alert.alert-danger Terminus could not start with your plugins, so all third party plugins have been disabled in this session. The error was:
+
+ pre {{error}}
+
+.modal-footer
+ button.btn.btn-outline-primary((click)='close()') Close
diff --git a/terminus-core/src/components/safeModeModal.component.ts b/terminus-core/src/components/safeModeModal.component.ts
new file mode 100644
index 00000000..33a5b605
--- /dev/null
+++ b/terminus-core/src/components/safeModeModal.component.ts
@@ -0,0 +1,19 @@
+import { Component, Input } from '@angular/core'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+
+@Component({
+ template: require('./safeModeModal.component.pug'),
+})
+export class SafeModeModalComponent {
+ @Input() error: Error
+
+ constructor (
+ public modalInstance: NgbActiveModal,
+ ) {
+ this.error = window['safeModeReason']
+ }
+
+ close () {
+ this.modalInstance.dismiss()
+ }
+}
diff --git a/terminus-core/src/components/startPage.component.pug b/terminus-core/src/components/startPage.component.pug
index 50ae8ef8..73e071f5 100644
--- a/terminus-core/src/components/startPage.component.pug
+++ b/terminus-core/src/components/startPage.component.pug
@@ -1,28 +1,15 @@
div
.terminus-logo
- .part(style='transform: rotateZ(0deg)')
- div
- .part(style='transform: rotateZ(51deg)')
- div
- .part(style='transform: rotateZ(102deg)')
- div
- .part(style='transform: rotateZ(154deg)')
- div
- .part(style='transform: rotateZ(205deg)')
- div
- .part(style='transform: rotateZ(257deg)')
- div
- .part(style='transform: rotateZ(308deg)')
- div
h1.terminus-title Terminus
- span.text-muted α
+ sup α
- button.btn.btn-primary.btn-lg.btn-block(
- *ngFor='let button of getButtons()',
- (click)='button.click()',
- )
- i.fa([class]='"fa fa-" + button.icon')
- span {{button.title}}
+ .list-group
+ a.list-group-item.list-group-item-action(
+ *ngFor='let button of getButtons()',
+ (click)='button.click()',
+ )
+ i([class]='"fa fa-fw fa-" + button.icon')
+ span {{button.title}}
footer
.pull-right
diff --git a/terminus-core/src/components/startPage.component.scss b/terminus-core/src/components/startPage.component.scss
index 315e81de..13fcace1 100644
--- a/terminus-core/src/components/startPage.component.scss
+++ b/terminus-core/src/components/startPage.component.scss
@@ -24,6 +24,6 @@ footer {
background: rgba(0,0,0,.5);
}
-button {
+a, button {
-webkit-app-region: no-drag;
}
diff --git a/terminus-core/src/components/tabHeader.component.pug b/terminus-core/src/components/tabHeader.component.pug
index c4eaf3bc..7db00c35 100644
--- a/terminus-core/src/components/tabHeader.component.pug
+++ b/terminus-core/src/components/tabHeader.component.pug
@@ -1,3 +1,3 @@
.index {{index + 1}}
-.name {{tab.title$ | async}}
+.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
button((click)='closeClicked.emit()') ×
diff --git a/terminus-core/src/components/tabHeader.component.ts b/terminus-core/src/components/tabHeader.component.ts
index f0c65524..0b47b1c8 100644
--- a/terminus-core/src/components/tabHeader.component.ts
+++ b/terminus-core/src/components/tabHeader.component.ts
@@ -1,5 +1,7 @@
import { Component, Input, Output, EventEmitter, HostBinding, HostListener } from '@angular/core'
-import { BaseTabComponent } from '../components/baseTab.component'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { BaseTabComponent } from './baseTab.component'
+import { RenameTabModalComponent } from './renameTabModal.component'
@Component({
selector: 'tab-header',
@@ -13,8 +15,20 @@ export class TabHeaderComponent {
@Input() tab: BaseTabComponent
@Output() closeClicked = new EventEmitter()
- @HostListener('auxclick', ['$event']) onClick ($event: MouseEvent): void {
- if ($event.which == 2) {
+ constructor (
+ private ngbModal: NgbModal,
+ ) { }
+
+ @HostListener('dblclick') onDoubleClick (): void {
+ let modal = this.ngbModal.open(RenameTabModalComponent)
+ modal.componentInstance.value = this.tab.customTitle || this.tab.title
+ modal.result.then(result => {
+ this.tab.customTitle = result
+ }).catch(() => null)
+ }
+
+ @HostListener('auxclick', ['$event']) onAuxClick ($event: MouseEvent): void {
+ if ($event.which === 2) {
this.closeClicked.emit()
}
}
diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts
index e5a0d9ee..f222abdb 100644
--- a/terminus-core/src/index.ts
+++ b/terminus-core/src/index.ts
@@ -11,17 +11,18 @@ import { ElectronService } from './services/electron.service'
import { HostAppService } from './services/hostApp.service'
import { LogService } from './services/log.service'
import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
-import { QuitterService } from './services/quitter.service'
import { DockingService } from './services/docking.service'
import { TabRecoveryService } from './services/tabRecovery.service'
import { ThemesService } from './services/themes.service'
import { AppRootComponent } from './components/appRoot.component'
import { TabBodyComponent } from './components/tabBody.component'
+import { SafeModeModalComponent } from './components/safeModeModal.component'
import { StartPageComponent } from './components/startPage.component'
import { TabHeaderComponent } from './components/tabHeader.component'
import { TitleBarComponent } from './components/titleBar.component'
import { WindowControlsComponent } from './components/windowControls.component'
+import { RenameTabModalComponent } from './components/renameTabModal.component'
import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider'
@@ -42,7 +43,6 @@ const PROVIDERS = [
LogService,
TabRecoveryService,
ThemesService,
- QuitterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
{ provide: Theme, useClass: StandardTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
@@ -65,7 +65,13 @@ const PROVIDERS = [
TabHeaderComponent,
TitleBarComponent,
WindowControlsComponent,
+ RenameTabModalComponent,
+ SafeModeModalComponent,
],
+ entryComponents: [
+ RenameTabModalComponent,
+ SafeModeModalComponent,
+ ]
})
export default class AppModule {
static forRoot (): ModuleWithProviders {
diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts
index fe72f0bb..757cae87 100644
--- a/terminus-core/src/services/app.service.ts
+++ b/terminus-core/src/services/app.service.ts
@@ -82,10 +82,16 @@ export class AppService {
}
}
- closeTab (tab: BaseTabComponent) {
+ async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise {
+ if (!this.tabs.includes(tab)) {
+ return
+ }
+ if (checkCanClose && !await tab.canClose()) {
+ return
+ }
+ this.tabs = this.tabs.filter((x) => x !== tab)
tab.destroy()
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
- this.tabs = this.tabs.filter((x) => x !== tab)
if (tab === this.activeTab) {
this.selectTab(this.tabs[newIndex])
}
diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts
index bfe9e923..c261f1ee 100644
--- a/terminus-core/src/services/config.service.ts
+++ b/terminus-core/src/services/config.service.ts
@@ -38,7 +38,7 @@ export class ConfigProxy {
{
enumerable: true,
configurable: false,
- get: () => real[key] || defaults[key],
+ get: () => (real[key] !== undefined) ? real[key] : defaults[key],
set: (value) => {
real[key] = value
}
diff --git a/terminus-core/src/services/docking.service.ts b/terminus-core/src/services/docking.service.ts
index 933232a7..c327cde6 100644
--- a/terminus-core/src/services/docking.service.ts
+++ b/terminus-core/src/services/docking.service.ts
@@ -40,12 +40,12 @@ export class DockingService {
newBounds.height = Math.round(fill * display.bounds.height)
}
if (dockSide === 'right') {
- newBounds.x = display.bounds.x + Math.round(display.bounds.width * (1.0 - fill))
+ newBounds.x = display.bounds.x + display.bounds.width - newBounds.width
} else {
newBounds.x = display.bounds.x
}
if (dockSide === 'bottom') {
- newBounds.y = display.bounds.y + Math.round(display.bounds.height * (1.0 - fill))
+ newBounds.y = display.bounds.y + display.bounds.height - newBounds.height
} else {
newBounds.y = display.bounds.y
}
diff --git a/terminus-core/src/services/electron.service.ts b/terminus-core/src/services/electron.service.ts
index 96214da3..3be9d648 100644
--- a/terminus-core/src/services/electron.service.ts
+++ b/terminus-core/src/services/electron.service.ts
@@ -27,4 +27,8 @@ export class ElectronService {
remoteRequire (name: string): any {
return this.remote.require(name)
}
+
+ remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
+ return this.remoteRequire(globals.require.resolve(`${plugin}/node_modules/${module}`))
+ }
}
diff --git a/terminus-core/src/services/hostApp.service.ts b/terminus-core/src/services/hostApp.service.ts
index 5c066963..4eccd72c 100644
--- a/terminus-core/src/services/hostApp.service.ts
+++ b/terminus-core/src/services/hostApp.service.ts
@@ -18,7 +18,6 @@ export interface Bounds {
export class HostAppService {
platform: Platform
nodePlatform: string
- quitRequested = new EventEmitter()
preferencesMenu$ = new Subject()
ready = new EventEmitter()
shown = new EventEmitter()
@@ -39,7 +38,6 @@ export class HostAppService {
linux: Platform.Linux
}[this.nodePlatform]
- electron.ipcRenderer.on('host:quit-request', () => this.zone.run(() => this.quitRequested.emit()))
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu$.next()))
electron.ipcRenderer.on('uncaughtException', ($event, err) => {
@@ -79,10 +77,6 @@ export class HostAppService {
this.getWindow().webContents.openDevTools()
}
- setCloseable (flag: boolean) {
- this.electron.ipcRenderer.send('window-set-closeable', flag)
- }
-
focusWindow () {
this.electron.ipcRenderer.send('window-focus')
}
diff --git a/terminus-core/src/services/log.service.ts b/terminus-core/src/services/log.service.ts
index a5931b50..fa3b4b10 100644
--- a/terminus-core/src/services/log.service.ts
+++ b/terminus-core/src/services/log.service.ts
@@ -5,14 +5,15 @@ export class Logger {
private name: string,
) {}
- log (level: string, ...args: any[]) {
+ doLog (level: string, ...args: any[]) {
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
}
- debug (...args: any[]) { this.log('debug', ...args) }
- info (...args: any[]) { this.log('info', ...args) }
- warn (...args: any[]) { this.log('warn', ...args) }
- error (...args: any[]) { this.log('error', ...args) }
+ debug (...args: any[]) { this.doLog('debug', ...args) }
+ info (...args: any[]) { this.doLog('info', ...args) }
+ warn (...args: any[]) { this.doLog('warn', ...args) }
+ error (...args: any[]) { this.doLog('error', ...args) }
+ log (...args: any[]) { this.doLog('log', ...args) }
}
@Injectable()
diff --git a/terminus-core/src/services/quitter.service.ts b/terminus-core/src/services/quitter.service.ts
deleted file mode 100644
index 5c8f8c65..00000000
--- a/terminus-core/src/services/quitter.service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Injectable } from '@angular/core'
-import { HostAppService } from '../services/hostApp.service'
-
-@Injectable()
-export class QuitterService {
- constructor (
- private hostApp: HostAppService,
- ) {
- hostApp.quitRequested.subscribe(() => {
- this.quit()
- })
- }
-
- quit () {
- this.hostApp.setCloseable(true)
- this.hostApp.quit()
- }
-}
diff --git a/terminus-core/src/services/tabRecovery.service.ts b/terminus-core/src/services/tabRecovery.service.ts
index 2661fbe8..4ce8e37f 100644
--- a/terminus-core/src/services/tabRecovery.service.ts
+++ b/terminus-core/src/services/tabRecovery.service.ts
@@ -1,5 +1,5 @@
import { Injectable, Inject } from '@angular/core'
-import { TabRecoveryProvider } from '../api/tabRecovery'
+import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service'
import { AppService } from '../services/app.service'
@@ -10,7 +10,7 @@ export class TabRecoveryService {
constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
- app: AppService,
+ private app: AppService,
log: LogService
) {
this.logger = log.create('tabRecovery')
@@ -29,15 +29,22 @@ export class TabRecoveryService {
async recoverTabs (): Promise {
if (window.localStorage.tabsRecovery) {
+ let tabs: RecoveredTab[] = []
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.tabRecoveryProviders) {
try {
- await provider.recover(token)
+ let tab = await provider.recover(token)
+ if (tab) {
+ tabs.push(tab)
+ }
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
}
+ tabs.forEach(tab => {
+ this.app.openNewTab(tab.type, tab.options)
+ })
}
}
diff --git a/terminus-core/src/services/themes.service.ts b/terminus-core/src/services/themes.service.ts
index 6e052664..5b199150 100644
--- a/terminus-core/src/services/themes.service.ts
+++ b/terminus-core/src/services/themes.service.ts
@@ -13,7 +13,6 @@ export class ThemesService {
this.applyCurrentTheme()
config.changed$.subscribe(() => {
this.applyCurrentTheme()
- document.querySelector('style#custom-css').innerHTML = config.store.appearance.css
})
}
@@ -32,6 +31,7 @@ export class ThemesService {
document.querySelector('head').appendChild(this.styleElement)
}
this.styleElement.textContent = theme.css
+ document.querySelector('style#custom-css').innerHTML = this.config.store.appearance.css
}
applyCurrentTheme (): void {
diff --git a/terminus-core/src/theme.scss b/terminus-core/src/theme.scss
index 2eb09bd7..93d13eee 100644
--- a/terminus-core/src/theme.scss
+++ b/terminus-core/src/theme.scss
@@ -23,6 +23,7 @@ $body-color: #aaa;
$font-family-sans-serif: "Source Sans Pro";
$font-size-base: 14rem / 16;
+$btn-border-radius: 0;
$btn-secondary-color: #ccc;
$btn-secondary-bg: #222;
$btn-secondary-border: #444;
@@ -70,7 +71,18 @@ $dropdown-link-disabled-color: #333;
$dropdown-header-color: #333;
$list-group-color: $body-color;
-$list-group-bg: $body-bg2;
+$list-group-bg: rgba(255,255,255,.05);
+$list-group-border-color: rgba(255,255,255,.1);
+$list-group-hover-bg: rgba(255,255,255,.1);
+$list-group-link-active-bg: rgba(255,255,255,.2);
+
+$pre-bg: $dropdown-bg;
+$pre-color: $dropdown-link-color;
+
+$alert-danger-bg: $body-bg2;
+$alert-danger-text: $red;
+$alert-danger-border: $red;
+
@import '~bootstrap/scss/bootstrap.scss';
@@ -270,12 +282,6 @@ hotkey-input-modal {
}
}
-start-page {
- .terminus-title {
- color: $blue;
- }
-}
-
.form-group label {
margin-bottom: 2px;
}
@@ -313,3 +319,11 @@ ngb-tabset .tab-content {
.input-group > select.form-control {
flex-direction: row;
}
+
+.list-group-item {
+ transition: 0.25s background;
+
+ i + * {
+ margin-left: 10px;
+ }
+}
diff --git a/terminus-plugin-manager/package.json b/terminus-plugin-manager/package.json
index 4225e4ed..6a14dc2f 100644
--- a/terminus-plugin-manager/package.json
+++ b/terminus-plugin-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "terminus-plugin-manager",
- "version": "1.0.0-alpha.16-8-gfc060ac",
+ "version": "1.0.0-alpha.24",
"description": "Terminus' plugin manager",
"keywords": [
"terminus-plugin"
@@ -19,10 +19,10 @@
"devDependencies": {
"@types/mz": "0.0.31",
"@types/node": "7.0.12",
- "@types/semver": "^5.3.31",
+ "@types/semver": "^5.3.32",
"@types/webpack-env": "1.13.0",
- "ngx-pipes": "^1.6.1",
"css-loader": "^0.28.0",
+ "ngx-pipes": "^1.6.1",
"semver": "^5.3.0"
},
"peerDependencies": {
diff --git a/terminus-settings/package.json b/terminus-settings/package.json
index ae878982..2de0e6f0 100644
--- a/terminus-settings/package.json
+++ b/terminus-settings/package.json
@@ -1,6 +1,6 @@
{
"name": "terminus-settings",
- "version": "1.0.0-alpha.16-8-gfc060ac",
+ "version": "1.0.0-alpha.24",
"description": "Terminus terminal settings page",
"keywords": [
"terminus-plugin"
diff --git a/terminus-settings/src/components/settingsTab.component.pug b/terminus-settings/src/components/settingsTab.component.pug
index e20606a3..57d5a90a 100644
--- a/terminus-settings/src/components/settingsTab.component.pug
+++ b/terminus-settings/src/components/settingsTab.component.pug
@@ -2,9 +2,9 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
ngb-tabset.vertical(type='tabs')
ngb-tab
- template(ngbTabTitle)
+ ng-template(ngbTabTitle)
| Application
- template(ngbTabContent)
+ ng-template(ngbTabContent)
.row
.col.col-lg-6
.form-group
@@ -153,9 +153,9 @@ ngb-tabset.vertical(type='tabs')
)
ngb-tab
- template(ngbTabTitle)
+ ng-template(ngbTabTitle)
| Hotkeys
- template(ngbTabContent)
+ ng-template(ngbTabContent)
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
.form-group
table.hotkeys-table
@@ -173,7 +173,7 @@ ngb-tabset.vertical(type='tabs')
)
ngb-tab(*ngFor='let provider of settingsProviders')
- template(ngbTabTitle)
+ ng-template(ngbTabTitle)
| {{provider.title}}
- template(ngbTabContent)
+ ng-template(ngbTabContent)
settings-tab-body([provider]='provider')
diff --git a/terminus-settings/src/components/settingsTab.component.ts b/terminus-settings/src/components/settingsTab.component.ts
index e3733121..00b83c93 100644
--- a/terminus-settings/src/components/settingsTab.component.ts
+++ b/terminus-settings/src/components/settingsTab.component.ts
@@ -27,7 +27,7 @@ export class SettingsTabComponent extends BaseTabComponent {
) {
super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
- this.title$.next('Settings')
+ this.title = 'Settings'
this.scrollable = true
this.screens = this.docking.getScreens()
}
diff --git a/terminus-settings/src/recoveryProvider.ts b/terminus-settings/src/recoveryProvider.ts
index 297ef589..442e7e2f 100644
--- a/terminus-settings/src/recoveryProvider.ts
+++ b/terminus-settings/src/recoveryProvider.ts
@@ -1,19 +1,14 @@
import { Injectable } from '@angular/core'
-import { TabRecoveryProvider, AppService } from 'terminus-core'
+import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
import { SettingsTabComponent } from './components/settingsTab.component'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
- constructor (
- private app: AppService
- ) {
- super()
- }
-
- async recover (recoveryToken: any): Promise {
+ async recover (recoveryToken: any): Promise {
if (recoveryToken.type === 'app:settings') {
- this.app.openNewTab(SettingsTabComponent)
+ return { type: SettingsTabComponent }
}
+ return null
}
}
diff --git a/terminus-terminal/package.json b/terminus-terminal/package.json
index 55b49a12..46a39639 100644
--- a/terminus-terminal/package.json
+++ b/terminus-terminal/package.json
@@ -1,6 +1,6 @@
{
"name": "terminus-terminal",
- "version": "1.0.0-alpha.16-8-gfc060ac",
+ "version": "1.0.0-alpha.24",
"description": "Terminus' terminal emulation core",
"keywords": [
"terminus-plugin"
@@ -23,7 +23,8 @@
"@types/webpack-env": "1.13.0",
"@types/winreg": "^1.2.30",
"dataurl": "0.1.0",
- "deep-equal": "1.0.1"
+ "deep-equal": "1.0.1",
+ "file-loader": "^0.11.2"
},
"peerDependencies": {
"@angular/common": "4.0.1",
@@ -31,15 +32,19 @@
"@angular/forms": "4.0.1",
"@angular/platform-browser": "4.0.1",
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
+ "rxjs": "5.3.0",
"terminus-core": "*",
- "terminus-settings": "*",
- "rxjs": "5.3.0"
+ "terminus-settings": "*"
},
"dependencies": {
+ "@types/async-lock": "0.0.19",
+ "async-lock": "^1.0.0",
"font-manager": "0.2.2",
- "hterm-umdjs": "1.2.0",
+ "hterm-umdjs": "1.1.3",
"mz": "^2.6.0",
"node-pty": "0.6.8",
+ "ps-node": "^0.1.6",
+ "runes": "^0.4.2",
"winreg": "^1.2.3"
},
"false": {}
diff --git a/terminus-terminal/src/api.ts b/terminus-terminal/src/api.ts
index 36163305..9e238ad8 100644
--- a/terminus-terminal/src/api.ts
+++ b/terminus-terminal/src/api.ts
@@ -1,6 +1,7 @@
import { Observable } from 'rxjs'
import { TerminalTabComponent } from './components/terminalTab.component'
export { TerminalTabComponent }
+export { IChildProcess } from './services/sessions.service'
export abstract class TerminalDecorator {
// tslint:disable-next-line no-empty
@@ -27,6 +28,10 @@ export interface SessionOptions {
}
export abstract class SessionPersistenceProvider {
+ abstract id: string
+ abstract displayName: string
+
+ abstract isAvailable (): boolean
abstract async attachSession (recoveryId: any): Promise
abstract async startSession (options: SessionOptions): Promise
abstract async terminateSession (recoveryId: string): Promise
@@ -43,3 +48,15 @@ export interface ITerminalColorScheme {
export abstract class TerminalColorSchemeProvider {
abstract async getSchemes (): Promise
}
+
+export interface IShell {
+ id: string
+ name: string
+ command: string
+ args?: string[]
+ env?: any
+}
+
+export abstract class ShellProvider {
+ abstract async provide (): Promise
+}
diff --git a/terminus-terminal/src/bell.ogg b/terminus-terminal/src/bell.ogg
new file mode 100644
index 00000000..d076186e
Binary files /dev/null and b/terminus-terminal/src/bell.ogg differ
diff --git a/terminus-terminal/src/buttonProvider.ts b/terminus-terminal/src/buttonProvider.ts
index ae81f1c8..ec1c2f40 100644
--- a/terminus-terminal/src/buttonProvider.ts
+++ b/terminus-terminal/src/buttonProvider.ts
@@ -1,24 +1,32 @@
+import { AsyncSubject } from 'rxjs'
import * as fs from 'mz/fs'
import * as path from 'path'
-import { Injectable } from '@angular/core'
-import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService, ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
+import { Injectable, Inject } from '@angular/core'
+import { HotkeysService, ToolbarButtonProvider, IToolbarButton, ConfigService, HostAppService, ElectronService, Logger, LogService } from 'terminus-core'
-import { SessionsService } from './services/sessions.service'
-import { ShellsService } from './services/shells.service'
-import { TerminalTabComponent } from './components/terminalTab.component'
+import { IShell, ShellProvider } from './api'
+import { TerminalService } from './services/terminal.service'
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
+ private shells$ = new AsyncSubject()
+ private logger: Logger
+
constructor (
- private app: AppService,
- private sessions: SessionsService,
+ private terminal: TerminalService,
private config: ConfigService,
- private shells: ShellsService,
- private hostApp: HostAppService,
+ log: LogService,
+ hostApp: HostAppService,
+ @Inject(ShellProvider) shellProviders: ShellProvider[],
electron: ElectronService,
hotkeys: HotkeysService,
) {
super()
+ this.logger = log.create('newTerminalButton')
+ Promise.all(shellProviders.map(x => x.provide())).then(shellLists => {
+ this.shells$.next(shellLists.reduce((a, b) => a.concat(b)))
+ this.shells$.complete()
+ })
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey === 'new-tab') {
this.openNewTab()
@@ -35,7 +43,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
if (!electron.remote.process.env.DEV) {
setImmediate(async () => {
let argv: string[] = electron.remote.process.argv
- for (let arg of argv.slice(1)) {
+ for (let arg of argv.slice(1).concat([electron.remote.process.argv0])) {
if (await fs.exists(arg)) {
if ((await fs.stat(arg)).isDirectory()) {
this.openNewTab(arg)
@@ -47,31 +55,9 @@ export class ButtonProvider extends ToolbarButtonProvider {
}
async openNewTab (cwd?: string): Promise {
- if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
- cwd = await this.app.activeTab.session.getWorkingDirectory()
- }
- let command = this.config.store.terminal.shell
- let env: any = {}
- let args: string[] = []
- if (command === '~clink~') {
- ({ command, args } = this.shells.getClinkOptions())
- }
- if (command === '~default-shell~') {
- command = await this.shells.getDefaultShell()
- }
- if (this.hostApp.platform === Platform.Windows) {
- env.TERM = 'cygwin'
- }
- let sessionOptions = await this.sessions.prepareNewSession({
- command,
- args,
- cwd,
- env,
- })
- this.app.openNewTab(
- TerminalTabComponent,
- { sessionOptions }
- )
+ let shells = await this.shells$.first().toPromise()
+ let shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
+ this.terminal.openTab(shell, cwd)
}
provide (): IToolbarButton[] {
diff --git a/terminus-terminal/src/components/terminalSettingsTab.component.pug b/terminus-terminal/src/components/terminalSettingsTab.component.pug
index ab915358..da2404fb 100644
--- a/terminus-terminal/src/components/terminalSettingsTab.component.pug
+++ b/terminus-terminal/src/components/terminalSettingsTab.component.pug
@@ -174,26 +174,53 @@
[title]='idx',
)
- .form-group
- label Terminal background
- br
- div(
- '[(ngModel)]'='config.store.terminal.background',
- (ngModelChange)='config.save()',
- ngbRadioGroup
- )
- label.btn.btn-secondary
- input(
- type='radio',
- [value]='"theme"'
- )
- | From theme
- label.btn.btn-secondary
- input(
- type='radio',
- [value]='"colorScheme"'
- )
- | From colors
+ .d-flex
+ .form-group.mr-3
+ label Terminal background
+ br
+ div(
+ '[(ngModel)]'='config.store.terminal.background',
+ (ngModelChange)='config.save()',
+ ngbRadioGroup
+ )
+ label.btn.btn-secondary
+ input(
+ type='radio',
+ [value]='"theme"'
+ )
+ | From theme
+ label.btn.btn-secondary
+ input(
+ type='radio',
+ [value]='"colorScheme"'
+ )
+ | From colors
+ .form-group
+ label Cursor shape
+ br
+ div(
+ [(ngModel)]='config.store.terminal.cursor',
+ (ngModelChange)='config.save()',
+ ngbRadioGroup
+ )
+ label.btn.btn-secondary
+ input(
+ type='radio',
+ [value]='"block"'
+ )
+ | █
+ label.btn.btn-secondary
+ input(
+ type='radio',
+ [value]='"beam"'
+ )
+ | |
+ label.btn.btn-secondary
+ input(
+ type='radio',
+ [value]='"underline"'
+ )
+ | ▁
.form-group
label Shell
@@ -203,7 +230,7 @@
)
option(
*ngFor='let shell of shells',
- [ngValue]='shell.command'
+ [ngValue]='shell.id'
) {{shell.name}}
.form-group
@@ -232,3 +259,15 @@
[value]='"audible"'
)
| Audible
+
+ .form-group
+ label Session persistence
+ select.form-control(
+ '[(ngModel)]'='config.store.terminal.persistence',
+ (ngModelChange)='config.save()',
+ )
+ option([ngValue]='null') Off
+ option(
+ *ngFor='let provider of persistenceProviders',
+ [ngValue]='provider.id'
+ ) {{provider.displayName}}
diff --git a/terminus-terminal/src/components/terminalSettingsTab.component.ts b/terminus-terminal/src/components/terminalSettingsTab.component.ts
index 94a3d231..3e91edcd 100644
--- a/terminus-terminal/src/components/terminalSettingsTab.component.ts
+++ b/terminus-terminal/src/components/terminalSettingsTab.component.ts
@@ -1,23 +1,11 @@
import { Observable } from 'rxjs'
-import * as fs from 'mz/fs'
-import * as path from 'path'
import { exec } from 'mz/child_process'
const equal = require('deep-equal')
const fontManager = require('font-manager')
import { Component, Inject } from '@angular/core'
import { ConfigService, HostAppService, Platform } from 'terminus-core'
-import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
-
-let Registry = null
-try {
- Registry = require('winreg')
-} catch (_) { } // tslint:disable-line no-empty
-
-interface IShell {
- name: string
- command: string
-}
+import { TerminalColorSchemeProvider, ITerminalColorScheme, IShell, ShellProvider, SessionPersistenceProvider } from '../api'
@Component({
template: require('./terminalSettingsTab.component.pug'),
@@ -26,6 +14,7 @@ interface IShell {
export class TerminalSettingsTabComponent {
fonts: string[] = []
shells: IShell[] = []
+ persistenceProviders: SessionPersistenceProvider[]
colorSchemes: ITerminalColorScheme[] = []
equalComparator = equal
editingColorScheme: ITerminalColorScheme
@@ -34,8 +23,12 @@ export class TerminalSettingsTabComponent {
constructor (
public config: ConfigService,
private hostApp: HostAppService,
+ @Inject(ShellProvider) private shellProviders: ShellProvider[],
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
- ) { }
+ @Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[],
+ ) {
+ this.persistenceProviders = persistenceProviders.filter(x => x.isAvailable())
+ }
async ngOnInit () {
if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
@@ -53,71 +46,8 @@ export class TerminalSettingsTabComponent {
this.fonts.sort()
})
}
- if (this.hostApp.platform === Platform.Windows) {
- this.shells = [
- { name: 'CMD (clink)', command: '~clink~' },
- { name: 'CMD (stock)', command: 'cmd.exe' },
- { name: 'PowerShell', command: 'powershell.exe' },
- ]
-
- // Detect whether BoW is installed
- const wslPath = `${process.env.windir}\\system32\\bash.exe`
- if (await fs.exists(wslPath)) {
- this.shells.push({ name: 'Bash on Windows', command: wslPath })
- }
-
- // Detect Cygwin
- let cygwinPath = await new Promise(resolve => {
- let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
- reg.get('rootdir', (err, item) => {
- if (err) {
- return resolve(null)
- }
- resolve(item.value)
- })
- })
- if (cygwinPath) {
- this.shells.push({ name: 'Cygwin', command: path.join(cygwinPath, 'bin', 'bash.exe') })
- }
-
- // Detect 32-bit Cygwin
- let cygwin32Path = await new Promise(resolve => {
- let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
- reg.get('rootdir', (err, item) => {
- if (err) {
- return resolve(null)
- }
- resolve(item.value)
- })
- })
- if (cygwin32Path) {
- this.shells.push({ name: 'Cygwin (32 bit)', command: path.join(cygwin32Path, 'bin', 'bash.exe') })
- }
-
- // Detect Git-Bash
- let gitBashPath = await new Promise(resolve => {
- let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
- reg.get('InstallPath', (err, item) => {
- if (err) {
- resolve(null)
- return
- }
- resolve(item.value)
- })
- })
- if (gitBashPath) {
- this.shells.push({ name: 'Git-Bash', command: path.join(gitBashPath, 'bin', 'bash.exe') })
- }
- }
- if (this.hostApp.platform === Platform.Linux || this.hostApp.platform === Platform.macOS) {
- this.shells = [{ name: 'Default shell', command: '~default-shell~' }]
- this.shells = this.shells.concat((await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
- .split('\n')
- .map(x => x.trim())
- .filter(x => x && !x.startsWith('#'))
- .map(x => ({ name: x, command: x })))
- }
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
+ this.shells = (await Promise.all(this.shellProviders.map(x => x.provide()))).reduce((a, b) => a.concat(b))
}
fontAutocomplete = (text$: Observable) => {
diff --git a/terminus-terminal/src/components/terminalTab.component.scss b/terminus-terminal/src/components/terminalTab.component.scss
index 5be43d0f..0556f84c 100644
--- a/terminus-terminal/src/components/terminalTab.component.scss
+++ b/terminus-terminal/src/components/terminalTab.component.scss
@@ -9,7 +9,7 @@
display: block;
overflow: hidden;
margin: 15px;
- transition: opacity ease-out 0.1s;
+ transition: opacity ease-out 0.25s;
opacity: 0;
div[style]:last-child {
diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts
index 9e506944..187a89ec 100644
--- a/terminus-terminal/src/components/terminalTab.component.ts
+++ b/terminus-terminal/src/components/terminalTab.component.ts
@@ -1,4 +1,3 @@
-const dataurl = require('dataurl')
import { BehaviorSubject, Subject, Subscription } from 'rxjs'
import 'rxjs/add/operator/bufferTime'
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
@@ -21,7 +20,6 @@ export class TerminalTabComponent extends BaseTabComponent {
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
hterm: any
- configSubscription: Subscription
sessionCloseSubscription: Subscription
hotkeysSubscription: Subscription
bell$ = new Subject()
@@ -33,6 +31,7 @@ export class TerminalTabComponent extends BaseTabComponent {
alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject()
htermVisible = false
+ private bellPlayer: HTMLAudioElement
private io: any
constructor (
@@ -47,14 +46,14 @@ export class TerminalTabComponent extends BaseTabComponent {
) {
super()
this.decorators = this.decorators || []
- this.title$.next('Terminal')
- this.configSubscription = config.changed$.subscribe(() => {
- this.configure()
- })
+ this.title = 'Terminal'
this.resize$.first().subscribe(async (resizeEvent) => {
this.session = this.sessions.addSession(
Object.assign({}, this.sessionOptions, resizeEvent)
)
+ setTimeout(() => {
+ this.session.resize(resizeEvent.width, resizeEvent.height)
+ }, 1000)
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => {
// let data = datas.join('')
@@ -88,6 +87,8 @@ export class TerminalTabComponent extends BaseTabComponent {
this.resetZoom()
}
})
+ this.bellPlayer = document.createElement('audio')
+ this.bellPlayer.src = require('../bell.ogg')
}
getRecoveryToken (): any {
@@ -99,6 +100,7 @@ export class TerminalTabComponent extends BaseTabComponent {
ngOnInit () {
this.focused$.subscribe(() => {
+ this.configure()
setTimeout(() => {
this.hterm.scrollPort_.resize()
this.hterm.scrollPort_.focus()
@@ -129,13 +131,15 @@ export class TerminalTabComponent extends BaseTabComponent {
}, 1000)
this.bell$.subscribe(() => {
- if (this.config.store.terminal.bell !== 'off') {
- let bg = preferenceManager.get('background-color')
+ if (this.config.store.terminal.bell === 'visual') {
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
setTimeout(() => {
- preferenceManager.set('background-color', bg)
+ this.configure()
}, 125)
}
+ if (this.config.store.terminal.bell === 'audible') {
+ this.bellPlayer.play()
+ }
// TODO audible
})
}
@@ -143,7 +147,7 @@ export class TerminalTabComponent extends BaseTabComponent {
attachHTermHandlers (hterm: any) {
hterm.setWindowTitle = (title) => {
this.zone.run(() => {
- this.title$.next(title)
+ this.title = title
})
}
@@ -155,6 +159,8 @@ export class TerminalTabComponent extends BaseTabComponent {
hterm.primaryScreen_.syncSelectionCaret = () => null
hterm.alternateScreen_.syncSelectionCaret = () => null
+ hterm.primaryScreen_.terminal = hterm
+ hterm.alternateScreen_.terminal = hterm
const _onPaste = hterm.scrollPort_.onPaste_.bind(hterm.scrollPort_)
hterm.scrollPort_.onPaste_ = (event) => {
@@ -208,6 +214,13 @@ export class TerminalTabComponent extends BaseTabComponent {
return ret
}
}
+
+ const _measureCharacterSize = hterm.scrollPort_.measureCharacterSize.bind(hterm.scrollPort_)
+ hterm.scrollPort_.measureCharacterSize = () => {
+ let size = _measureCharacterSize()
+ size.height += this.config.store.terminal.linePadding
+ return size
+ }
}
attachIOHandlers (io: any) {
@@ -244,10 +257,10 @@ export class TerminalTabComponent extends BaseTabComponent {
async configure (): Promise {
let config = this.config.store
- preferenceManager.set('font-family', config.terminal.font)
+ preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
this.setFontSize()
preferenceManager.set('enable-bold', true)
- preferenceManager.set('audible-bell-sound', '')
+ // preferenceManager.set('audible-bell-sound', '')
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
preferenceManager.set('enable-clipboard-notice', false)
preferenceManager.set('receive-encoding', 'raw')
@@ -294,13 +307,15 @@ export class TerminalTabComponent extends BaseTabComponent {
}
`
}
- preferenceManager.set('user-css', dataurl.convert({
- data: css,
- mimetype: 'text/css',
- charset: 'utf8',
- }))
-
+ css += config.appearance.css
+ this.hterm.setCSS(css)
this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
+ this.hterm.defaultCursorShape = {
+ block: hterm.hterm.Terminal.cursorShape.BLOCK,
+ underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
+ beam: hterm.hterm.Terminal.cursorShape.BEAM,
+ }[config.terminal.cursor]
+ this.hterm.applyCursorShape()
}
zoomIn () {
@@ -322,7 +337,6 @@ export class TerminalTabComponent extends BaseTabComponent {
this.decorators.forEach(decorator => {
decorator.detach(this)
})
- this.configSubscription.unsubscribe()
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
@@ -343,6 +357,17 @@ export class TerminalTabComponent extends BaseTabComponent {
}
}
+ async canClose (): Promise {
+ if (this.hostApp.platform === Platform.Windows) {
+ return true
+ }
+ let children = await this.session.getChildProcesses()
+ if (children.length === 0) {
+ return true
+ }
+ return confirm(`"${children[0].command}" is still running. Close?`)
+ }
+
private setFontSize () {
preferenceManager.set('font-size', this.config.store.terminal.fontSize * Math.pow(1.1, this.zoom))
}
diff --git a/terminus-terminal/src/config.ts b/terminus-terminal/src/config.ts
index f36103bb..e85c2a21 100644
--- a/terminus-terminal/src/config.ts
+++ b/terminus-terminal/src/config.ts
@@ -4,10 +4,12 @@ export class TerminalConfigProvider extends ConfigProvider {
defaults = {
terminal: {
fontSize: 14,
+ linePadding: 0,
bell: 'off',
bracketedPaste: false,
background: 'theme',
ligatures: false,
+ cursor: 'block',
colorScheme: {
__nonStructural: true,
name: 'Material',
@@ -41,7 +43,8 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.macOS]: {
terminal: {
font: 'Menlo',
- shell: '~default-shell~',
+ shell: 'default',
+ persistence: 'screen',
},
hotkeys: {
'copy': [
@@ -72,7 +75,8 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.Windows]: {
terminal: {
font: 'Consolas',
- shell: '~clink~',
+ shell: 'clink',
+ persistence: null,
},
hotkeys: {
'copy': [
@@ -102,7 +106,8 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.Linux]: {
terminal: {
font: 'Liberation Mono',
- shell: '~default-shell~',
+ shell: 'default',
+ persistence: 'tmux',
},
hotkeys: {
'copy': [
diff --git a/terminus-terminal/src/fonts/Meslo.otf b/terminus-terminal/src/fonts/Meslo.otf
new file mode 100644
index 00000000..710d5b1a
Binary files /dev/null and b/terminus-terminal/src/fonts/Meslo.otf differ
diff --git a/terminus-terminal/src/hterm.ts b/terminus-terminal/src/hterm.ts
index f0e6617f..2c1454fa 100644
--- a/terminus-terminal/src/hterm.ts
+++ b/terminus-terminal/src/hterm.ts
@@ -22,6 +22,16 @@ preferenceManager.set('color-palette-overrides', {
hterm.hterm.Terminal.prototype.showOverlay = () => null
+hterm.hterm.Terminal.prototype.setCSS = function (css) {
+ const doc = this.scrollPort_.document_
+ if (!doc.querySelector('#user-css')) {
+ const node = doc.createElement('style')
+ node.id = 'user-css'
+ doc.head.appendChild(node)
+ }
+ doc.querySelector('#user-css').innerText = css
+}
+
const oldCharWidthDisregardAmbiguous = hterm.lib.wc.charWidthDisregardAmbiguous
hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => {
if ((codepoint >= 0x1f300 && codepoint <= 0x1f64f) ||
@@ -30,3 +40,38 @@ hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => {
}
return oldCharWidthDisregardAmbiguous(codepoint)
}
+
+hterm.hterm.Terminal.prototype.applyCursorShape = function () {
+ let modes = [
+ [hterm.hterm.Terminal.cursorShape.BLOCK, true],
+ [this.defaultCursorShape || hterm.hterm.Terminal.cursorShape.BLOCK, false],
+ [hterm.hterm.Terminal.cursorShape.BLOCK, false],
+ [hterm.hterm.Terminal.cursorShape.UNDERLINE, true],
+ [hterm.hterm.Terminal.cursorShape.UNDERLINE, false],
+ [hterm.hterm.Terminal.cursorShape.BEAM, true],
+ [hterm.hterm.Terminal.cursorShape.BEAM, false],
+ ]
+ let modeNumber = this.cursorMode || 1
+ console.log('mode', modeNumber)
+ if (modeNumber >= modes.length) {
+ console.warn('Unknown cursor style: ' + modeNumber)
+ return
+ }
+ this.setCursorShape(modes[modeNumber][0])
+ this.setCursorBlink(modes[modeNumber][1])
+}
+
+hterm.hterm.VT.CSI[' q'] = function (parseState) {
+ const arg = parseState.args[0]
+ this.terminal.cursorMode = arg
+ this.terminal.applyCursorShape()
+}
+
+const _collapseToEnd = Selection.prototype.collapseToEnd
+Selection.prototype.collapseToEnd = function () {
+ try {
+ _collapseToEnd.apply(this)
+ } catch (err) {
+ // tslint-disable-line
+ }
+}
diff --git a/terminus-terminal/src/hterm.userCSS.scss b/terminus-terminal/src/hterm.userCSS.scss
index 662fef2c..a279dd58 100644
--- a/terminus-terminal/src/hterm.userCSS.scss
+++ b/terminus-terminal/src/hterm.userCSS.scss
@@ -9,3 +9,13 @@ a:hover {
x-screen {
transition: 0.125s ease background;
}
+
+x-row > span {
+ display: inline-block;
+ height: inherit;
+}
+
+@font-face {
+ font-family: "monospace-fallback";
+ src: url(fonts/Meslo.otf) format("opentype");
+}
diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts
index 68e97efa..714a07a4 100644
--- a/terminus-terminal/src/index.ts
+++ b/terminus-terminal/src/index.ts
@@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
-import { HostAppService, Platform, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider } from 'terminus-core'
+import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
import { TerminalTabComponent } from './components/terminalTab.component'
@@ -11,17 +11,28 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
import { ColorPickerComponent } from './components/colorPicker.component'
import { SessionsService } from './services/sessions.service'
-import { ShellsService } from './services/shells.service'
+import { TerminalService } from './services/terminal.service'
-import { ScreenPersistenceProvider } from './persistenceProviders'
+import { ScreenPersistenceProvider } from './persistence/screen'
+import { TMuxPersistenceProvider } from './persistence/tmux'
import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider'
-import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator } from './api'
+import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
import { TerminalSettingsTabProvider } from './settings'
import { PathDropDecorator } from './pathDrop'
import { TerminalConfigProvider } from './config'
import { TerminalHotkeyProvider } from './hotkeys'
import { HyperColorSchemes } from './colorSchemes'
+
+import { Cygwin32ShellProvider } from './shells/cygwin32'
+import { Cygwin64ShellProvider } from './shells/cygwin64'
+import { GitBashShellProvider } from './shells/gitBash'
+import { LinuxDefaultShellProvider } from './shells/linuxDefault'
+import { MacOSDefaultShellProvider } from './shells/macDefault'
+import { POSIXShellsProvider } from './shells/posix'
+import { WindowsStockShellsProvider } from './shells/windowsStock'
+import { WSLShellProvider } from './shells/wsl'
+
import { hterm } from './hterm'
@NgModule({
@@ -32,26 +43,27 @@ import { hterm } from './hterm'
],
providers: [
SessionsService,
- ShellsService,
- ScreenPersistenceProvider,
+ TerminalService,
+
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
- {
- provide: SessionPersistenceProvider,
- useFactory: (hostApp: HostAppService, screen: ScreenPersistenceProvider) => {
- if (hostApp.platform === Platform.Windows) {
- return null
- } else {
- return screen
- }
- },
- deps: [HostAppService, ScreenPersistenceProvider],
- },
{ provide: SettingsTabProvider, useClass: TerminalSettingsTabProvider, multi: true },
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
{ provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
+
+ { provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider, multi: true },
+ { provide: SessionPersistenceProvider, useClass: TMuxPersistenceProvider, multi: true },
+
+ { provide: ShellProvider, useClass: WindowsStockShellsProvider, multi: true },
+ { provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
+ { provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
+ { provide: ShellProvider, useClass: Cygwin32ShellProvider, multi: true },
+ { provide: ShellProvider, useClass: Cygwin64ShellProvider, multi: true },
+ { provide: ShellProvider, useClass: GitBashShellProvider, multi: true },
+ { provide: ShellProvider, useClass: POSIXShellsProvider, multi: true },
+ { provide: ShellProvider, useClass: WSLShellProvider, multi: true },
],
entryComponents: [
TerminalTabComponent,
@@ -93,3 +105,4 @@ export default class TerminalModule {
}
export * from './api'
+export { TerminalService }
diff --git a/terminus-terminal/src/pathDrop.ts b/terminus-terminal/src/pathDrop.ts
index 8196c3cf..5514d602 100644
--- a/terminus-terminal/src/pathDrop.ts
+++ b/terminus-terminal/src/pathDrop.ts
@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
import { TerminalDecorator } from './api'
import { TerminalTabComponent } from './components/terminalTab.component'
-
@Injectable()
export class PathDropDecorator extends TerminalDecorator {
attach (terminal: TerminalTabComponent): void {
diff --git a/terminus-terminal/src/persistenceProviders.ts b/terminus-terminal/src/persistence/screen.ts
similarity index 70%
rename from terminus-terminal/src/persistenceProviders.ts
rename to terminus-terminal/src/persistence/screen.ts
index 141154ae..8f904d5f 100644
--- a/terminus-terminal/src/persistenceProviders.ts
+++ b/terminus-terminal/src/persistence/screen.ts
@@ -1,11 +1,12 @@
import * as fs from 'mz/fs'
+import * as path from 'path'
import { exec, spawn } from 'mz/child_process'
-import { exec as execCallback } from 'child_process'
+import { exec as execAsync, execFileSync } from 'child_process'
import { AsyncSubject } from 'rxjs'
import { Injectable } from '@angular/core'
-import { Logger, LogService } from 'terminus-core'
-import { SessionOptions, SessionPersistenceProvider } from './api'
+import { Logger, LogService, ElectronService } from 'terminus-core'
+import { SessionOptions, SessionPersistenceProvider } from '../api'
declare function delay (ms: number): Promise
@@ -29,18 +30,30 @@ async function listProcesses (): Promise {
@Injectable()
export class ScreenPersistenceProvider extends SessionPersistenceProvider {
+ id = 'screen'
+ displayName = 'GNU Screen'
private logger: Logger
constructor (
log: LogService,
+ private electron: ElectronService,
) {
super()
this.logger = log.create('main')
}
+ isAvailable () {
+ try {
+ execFileSync('sh', ['-c', 'which screen'])
+ return true
+ } catch (_) {
+ return false
+ }
+ }
+
async attachSession (recoveryId: any): Promise {
let lines = await new Promise(resolve => {
- execCallback('screen -list', (_err, stdout) => {
+ execAsync('screen -list', (_err, stdout) => {
// returns an error code on macOS
resolve(stdout.split('\n'))
})
@@ -64,12 +77,13 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
recoveryId,
recoveredTruePID$: truePID$.asObservable(),
command: 'screen',
- args: ['-r', recoveryId],
+ args: ['-d', '-r', recoveryId, '-c', await this.prepareConfig()],
}
}
async extractShellPID (screenPID: number): Promise {
- let child = (await listProcesses()).find(x => x.ppid === screenPID)
+ let processes = await listProcesses()
+ let child = processes.find(x => x.ppid === screenPID)
if (!child) {
throw new Error(`Could not find any children of the screen process (PID ${screenPID})!`)
@@ -77,32 +91,15 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
if (child.command === 'login') {
await delay(1000)
- child = (await listProcesses()).find(x => x.ppid === child.pid)
+ child = processes.find(x => x.ppid === child.pid)
}
return child.pid
}
async startSession (options: SessionOptions): Promise {
- let configPath = '/tmp/.termScreenConfig'
- await fs.writeFile(configPath, `
- escape ^^^
- vbell on
- deflogin on
- term xterm-color
- bindkey "^[OH" beginning-of-line
- bindkey "^[OF" end-of-line
- bindkey "\\027[?1049h" stuff ----alternate enter-----
- bindkey "\\027[?1049l" stuff ----alternate leave-----
- termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
- defhstatus "^Et"
- hardstatus off
- altscreen on
- defutf8 on
- defencoding utf8
- `, 'utf-8')
let recoveryId = `term-tab-${Date.now()}`
- let args = ['-d', '-m', '-c', configPath, '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', '-' + options.command].concat(options.args || [])
+ let args = ['-d', '-m', '-c', await this.prepareConfig(), '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', '-' + options.command].concat(options.args || [])
this.logger.debug('Spawning screen with', args.join(' '))
await spawn('screen', args, {
cwd: options.cwd,
@@ -118,4 +115,28 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
// screen has already quit
}
}
+
+ private async prepareConfig (): Promise {
+ let configPath = path.join(this.electron.app.getPath('userData'), 'screen-config.tmp')
+ await fs.writeFile(configPath, `
+ escape ^^^
+ vbell off
+ deflogin on
+ defflow off
+ term xterm-color
+ bindkey "^[OH" beginning-of-line
+ bindkey "^[OF" end-of-line
+ bindkey "^[[H" beginning-of-line
+ bindkey "^[[F" end-of-line
+ bindkey "\\027[?1049h" stuff ----alternate enter-----
+ bindkey "\\027[?1049l" stuff ----alternate leave-----
+ termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
+ defhstatus "^Et"
+ hardstatus off
+ altscreen on
+ defutf8 on
+ defencoding utf8
+ `, 'utf-8')
+ return configPath
+ }
}
diff --git a/terminus-terminal/src/persistence/tmux.ts b/terminus-terminal/src/persistence/tmux.ts
new file mode 100644
index 00000000..56d36339
--- /dev/null
+++ b/terminus-terminal/src/persistence/tmux.ts
@@ -0,0 +1,226 @@
+import { Injectable } from '@angular/core'
+import { execFileSync } from 'child_process'
+import * as AsyncLock from 'async-lock'
+import { ConnectableObservable, AsyncSubject, Subject } from 'rxjs'
+import * as childProcess from 'child_process'
+import { SessionOptions, SessionPersistenceProvider } from '../api'
+
+const TMUX_CONFIG = `
+ set -g status off
+ set -g focus-events on
+ set -g bell-action any
+ set -g bell-on-alert on
+ set -g visual-bell off
+ set -g set-titles on
+ set -g set-titles-string "#W"
+ set -g window-status-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
+ set -g window-status-current-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
+ set-option -g prefix C-^
+ set-option -g status-interval 1
+`
+
+export class TMuxBlock {
+ time: number
+ number: number
+ error: boolean
+ lines: string[]
+
+ constructor (line: string) {
+ this.time = parseInt(line.split(' ')[1])
+ this.number = parseInt(line.split(' ')[2])
+ this.lines = []
+ }
+}
+
+export class TMuxMessage {
+ type: string
+ content: string
+
+ constructor (line: string) {
+ this.type = line.substring(0, line.indexOf(' '))
+ this.content = line.substring(line.indexOf(' ') + 1)
+ }
+}
+
+export class TMuxCommandProcess {
+ private process: childProcess.ChildProcess
+ private rawOutput$ = new Subject()
+ private line$ = new Subject()
+ private message$ = new Subject()
+ private block$ = new Subject()
+ private response$: ConnectableObservable
+ private lock = new AsyncLock({ timeout: 1000 })
+
+ constructor () {
+ this.process = childProcess.spawn('tmux', ['-C', '-f', '/dev/null', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control'])
+ console.log('[tmux] started')
+ this.process.stdout.on('data', data => {
+ // console.debug('tmux says:', data.toString())
+ this.rawOutput$.next(data.toString())
+ })
+
+ let rawBuffer = ''
+ this.rawOutput$.subscribe(raw => {
+ rawBuffer += raw
+ if (rawBuffer.includes('\n')) {
+ let lines = rawBuffer.split('\n')
+ rawBuffer = lines.pop()
+ lines.forEach(line => this.line$.next(line))
+ }
+ })
+
+ let currentBlock = null
+ this.line$.subscribe(line => {
+ if (currentBlock) {
+ if (line.startsWith('%end ')) {
+ let block = currentBlock
+ currentBlock = null
+ setImmediate(() => {
+ this.block$.next(block)
+ })
+ } else if (line.startsWith('%error ')) {
+ let block = currentBlock
+ block.error = true
+ currentBlock = null
+ setImmediate(() => {
+ this.block$.next(block)
+ })
+ } else {
+ currentBlock.lines.push(line)
+ }
+ } else {
+ if (line.startsWith('%begin ')) {
+ currentBlock = new TMuxBlock(line)
+ } else {
+ this.message$.next(line)
+ }
+ }
+ })
+
+ this.response$ = this.block$.skip(1).publish()
+ this.response$.connect()
+
+ this.block$.subscribe(block => {
+ console.debug('[tmux] block:', block)
+ })
+
+ this.message$.subscribe(message => {
+ console.debug('[tmux] message:', message)
+ })
+ }
+
+ command (command: string): Promise {
+ return this.lock.acquire('key', () => {
+ let p = this.response$.take(1).toPromise()
+ console.debug('[tmux] command:', command)
+ this.process.stdin.write(command + '\n')
+ return p
+ }).then(response => {
+ if (response.error) {
+ throw response
+ }
+ return response
+ }) as Promise
+ }
+
+ destroy () {
+ this.rawOutput$.complete()
+ this.line$.complete()
+ this.block$.complete()
+ this.message$.complete()
+ this.process.kill('SIGTERM')
+ }
+}
+
+export class TMux {
+ private process: TMuxCommandProcess
+
+ constructor () {
+ this.process = new TMuxCommandProcess()
+ TMUX_CONFIG.split('\n').filter(x => x).forEach(async (line) => {
+ await this.process.command(line)
+ })
+ }
+
+ async create (id: string, options: SessionOptions): Promise {
+ let args = [options.command].concat(options.args)
+ let cmd = args.map(x => `"${x.replace('"', '\\"')}"`)
+ await this.process.command(
+ `new-session -s "${id}" -d`
+ + (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '')
+ + ` '${cmd}'`
+ )
+ }
+
+ async list (): Promise {
+ let block = await this.process.command('list-sessions -F "#{session_name}"')
+ return block.lines
+ }
+
+ async getPID (id: string): Promise {
+ let response = await this.process.command(`list-panes -t ${id} -F "#{pane_pid}"`)
+ if (response.lines.length === 0) {
+ return null
+ } else {
+ return parseInt(response.lines[0])
+ }
+ }
+
+ async terminate (id: string): Promise {
+ this.process.command(`kill-session -t ${id}`).catch(() => {
+ console.debug('Session already killed')
+ })
+ }
+}
+
+@Injectable()
+export class TMuxPersistenceProvider extends SessionPersistenceProvider {
+ id = 'tmux'
+ displayName = 'Tmux'
+ private tmux: TMux
+
+ constructor () {
+ super()
+ if (this.isAvailable()) {
+ this.tmux = new TMux()
+ }
+ }
+
+ isAvailable (): boolean {
+ try {
+ execFileSync('tmux', ['-V'])
+ return true
+ } catch (_) {
+ return false
+ }
+ }
+
+ async attachSession (recoveryId: any): Promise {
+ let sessions = await this.tmux.list()
+ if (!sessions.includes(recoveryId)) {
+ return null
+ }
+ let truePID$ = new AsyncSubject()
+ this.tmux.getPID(recoveryId).then(pid => {
+ truePID$.next(pid)
+ truePID$.complete()
+ })
+ return {
+ command: 'tmux',
+ args: ['-L', 'terminus', 'attach-session', '-d', '-t', recoveryId, ';', 'refresh-client'],
+ recoveredTruePID$: truePID$.asObservable(),
+ recoveryId,
+ }
+ }
+
+ async startSession (options: SessionOptions): Promise {
+ // TODO env
+ let recoveryId = Date.now().toString()
+ await this.tmux.create(recoveryId, options)
+ return recoveryId
+ }
+
+ async terminateSession (recoveryId: string): Promise {
+ await this.tmux.terminate(recoveryId)
+ }
+}
diff --git a/terminus-terminal/src/recoveryProvider.ts b/terminus-terminal/src/recoveryProvider.ts
index 3dc128ad..e141bbaf 100644
--- a/terminus-terminal/src/recoveryProvider.ts
+++ b/terminus-terminal/src/recoveryProvider.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
-import { TabRecoveryProvider, AppService } from 'terminus-core'
+import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
import { TerminalTabComponent } from './components/terminalTab.component'
import { SessionsService } from './services/sessions.service'
@@ -8,18 +8,21 @@ import { SessionsService } from './services/sessions.service'
export class RecoveryProvider extends TabRecoveryProvider {
constructor (
private sessions: SessionsService,
- private app: AppService,
) {
super()
}
- async recover (recoveryToken: any): Promise {
+ async recover (recoveryToken: any): Promise {
if (recoveryToken.type === 'app:terminal') {
let sessionOptions = await this.sessions.recover(recoveryToken.recoveryId)
if (!sessionOptions) {
- return
+ return null
+ }
+ return {
+ type: TerminalTabComponent,
+ options: { sessionOptions },
}
- this.app.openNewTab(TerminalTabComponent, { sessionOptions })
}
+ return null
}
}
diff --git a/terminus-terminal/src/services/sessions.service.ts b/terminus-terminal/src/services/sessions.service.ts
index 79231a00..2d931c53 100644
--- a/terminus-terminal/src/services/sessions.service.ts
+++ b/terminus-terminal/src/services/sessions.service.ts
@@ -1,12 +1,20 @@
-import * as nodePTY from 'node-pty'
+const psNode = require('ps-node')
+// import * as nodePTY from 'node-pty'
+let nodePTY
import * as fs from 'mz/fs'
import { Subject } from 'rxjs'
-import { Injectable } from '@angular/core'
-import { Logger, LogService } from 'terminus-core'
+import { Injectable, Inject } from '@angular/core'
+import { Logger, LogService, ElectronService, ConfigService } from 'terminus-core'
import { exec } from 'mz/child_process'
import { SessionOptions, SessionPersistenceProvider } from '../api'
+export interface IChildProcess {
+ pid: number
+ ppid: number
+ command: string
+}
+
export class Session {
open: boolean
name: string
@@ -101,6 +109,20 @@ export class Session {
this.pty.kill(signal)
}
+ async getChildProcesses (): Promise {
+ if (!this.truePID) {
+ return []
+ }
+ return new Promise((resolve, reject) => {
+ psNode.lookup({ ppid: this.truePID }, (err, processes) => {
+ if (err) {
+ return reject(err)
+ }
+ resolve(processes as IChildProcess[])
+ })
+ })
+ }
+
async gracefullyKillProcess (): Promise {
if (process.platform === 'win32') {
this.kill()
@@ -156,16 +178,21 @@ export class SessionsService {
private lastID = 0
constructor (
- private persistence: SessionPersistenceProvider,
+ @Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
+ private config: ConfigService,
+ electron: ElectronService,
log: LogService,
) {
+ nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty', global as any)
this.logger = log.create('sessions')
+ this.persistenceProviders = this.persistenceProviders.filter(x => x.isAvailable())
}
async prepareNewSession (options: SessionOptions): Promise {
- if (this.persistence) {
- let recoveryId = await this.persistence.startSession(options)
- options = await this.persistence.attachSession(recoveryId)
+ let persistence = this.getPersistence()
+ if (persistence) {
+ let recoveryId = await persistence.startSession(options)
+ options = await persistence.attachSession(recoveryId)
}
return options
}
@@ -174,10 +201,11 @@ export class SessionsService {
this.lastID++
options.name = `session-${this.lastID}`
let session = new Session(options)
+ let persistence = this.getPersistence()
session.destroyed$.first().subscribe(() => {
delete this.sessions[session.name]
- if (this.persistence) {
- this.persistence.terminateSession(session.recoveryId)
+ if (persistence) {
+ persistence.terminateSession(session.recoveryId)
}
})
this.sessions[session.name] = session
@@ -185,9 +213,17 @@ export class SessionsService {
}
async recover (recoveryId: string): Promise {
- if (!this.persistence) {
+ let persistence = this.getPersistence()
+ if (persistence) {
+ return await persistence.attachSession(recoveryId)
+ }
+ return null
+ }
+
+ private getPersistence (): SessionPersistenceProvider {
+ if (!this.config.store.terminal.persistence) {
return null
}
- return await this.persistence.attachSession(recoveryId)
+ return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
}
}
diff --git a/terminus-terminal/src/services/shells.service.ts b/terminus-terminal/src/services/shells.service.ts
deleted file mode 100644
index d63a7673..00000000
--- a/terminus-terminal/src/services/shells.service.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import * as path from 'path'
-import { exec } from 'mz/child_process'
-import * as fs from 'mz/fs'
-import { Injectable } from '@angular/core'
-import { ElectronService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
-
-@Injectable()
-export class ShellsService {
- private logger: Logger
-
- constructor (
- log: LogService,
- private electron: ElectronService,
- private hostApp: HostAppService,
- ) {
- this.logger = log.create('shells')
- }
-
- getClinkOptions (): { command, args } {
- return {
- command: 'cmd.exe',
- args: [
- '/k',
- path.join(
- path.dirname(this.electron.app.getPath('exe')),
- 'resources',
- 'clink',
- `clink_${process.arch}.exe`,
- ),
- 'inject',
- ]
- }
- }
-
- async getDefaultShell (): Promise {
- if (this.hostApp.platform === Platform.macOS) {
- return this.getDefaultMacOSShell()
- } else {
- return this.getDefaultLinuxShell()
- }
- }
-
- async getDefaultMacOSShell (): Promise {
- let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
- return shellEntry.split(' ')[1].trim()
- }
-
- async getDefaultLinuxShell (): Promise {
- let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
- .split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
- if (!line) {
- this.logger.warn('Could not detect user shell')
- return '/bin/sh'
- } else {
- return line.split(':')[6]
- }
- }
-}
diff --git a/terminus-terminal/src/services/terminal.service.ts b/terminus-terminal/src/services/terminal.service.ts
new file mode 100644
index 00000000..4e7d0fc9
--- /dev/null
+++ b/terminus-terminal/src/services/terminal.service.ts
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core'
+import { AppService, Logger, LogService } from 'terminus-core'
+import { IShell } from '../api'
+import { SessionsService } from './sessions.service'
+import { TerminalTabComponent } from '../components/terminalTab.component'
+
+@Injectable()
+export class TerminalService {
+ private logger: Logger
+
+ constructor (
+ private app: AppService,
+ private sessions: SessionsService,
+ log: LogService,
+ ) {
+ this.logger = log.create('terminal')
+ }
+
+ async openTab (shell: IShell, cwd?: string): Promise {
+ if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
+ cwd = await this.app.activeTab.session.getWorkingDirectory()
+ }
+ let env: any = Object.assign({}, process.env, shell.env || {})
+
+ this.logger.log(`Starting shell ${shell.name}`, shell)
+ let sessionOptions = await this.sessions.prepareNewSession({
+ command: shell.command,
+ args: shell.args || [],
+ cwd,
+ env,
+ })
+
+ this.logger.log('Using session options:', sessionOptions)
+
+ return this.app.openNewTab(
+ TerminalTabComponent,
+ { sessionOptions }
+ ) as TerminalTabComponent
+ }
+}
diff --git a/terminus-terminal/src/shells/cygwin32.ts b/terminus-terminal/src/shells/cygwin32.ts
new file mode 100644
index 00000000..3c0ce996
--- /dev/null
+++ b/terminus-terminal/src/shells/cygwin32.ts
@@ -0,0 +1,48 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+ Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class Cygwin32ShellProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Windows) {
+ return []
+ }
+
+ let cygwinPath = await new Promise(resolve => {
+ let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
+ reg.get('rootdir', (err, item) => {
+ if (err || !item) {
+ return resolve(null)
+ }
+ resolve(item.value)
+ })
+ })
+
+ if (!cygwinPath) {
+ return []
+ }
+
+ return [{
+ id: 'cygwin32',
+ name: 'Cygwin (32 bit)',
+ command: path.join(cygwinPath, 'bin', 'bash.exe'),
+ env: {
+ TERM: 'cygwin',
+ }
+ }]
+ }
+}
diff --git a/terminus-terminal/src/shells/cygwin64.ts b/terminus-terminal/src/shells/cygwin64.ts
new file mode 100644
index 00000000..2d3f73cb
--- /dev/null
+++ b/terminus-terminal/src/shells/cygwin64.ts
@@ -0,0 +1,48 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+ Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class Cygwin64ShellProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Windows) {
+ return []
+ }
+
+ let cygwinPath = await new Promise(resolve => {
+ let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
+ reg.get('rootdir', (err, item) => {
+ if (err || !item) {
+ return resolve(null)
+ }
+ resolve(item.value)
+ })
+ })
+
+ if (!cygwinPath) {
+ return []
+ }
+
+ return [{
+ id: 'cygwin64',
+ name: 'Cygwin',
+ command: path.join(cygwinPath, 'bin', 'bash.exe'),
+ env: {
+ TERM: 'cygwin',
+ }
+ }]
+ }
+}
diff --git a/terminus-terminal/src/shells/gitBash.ts b/terminus-terminal/src/shells/gitBash.ts
new file mode 100644
index 00000000..43f2995d
--- /dev/null
+++ b/terminus-terminal/src/shells/gitBash.ts
@@ -0,0 +1,63 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+let Registry = null
+try {
+ Registry = require('winreg')
+} catch (_) { } // tslint:disable-line no-empty
+
+@Injectable()
+export class GitBashShellProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Windows) {
+ return []
+ }
+
+ let gitBashPath = await new Promise(resolve => {
+ let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
+ reg.get('InstallPath', (err, item) => {
+ if (err || !item) {
+ resolve(null)
+ return
+ }
+ resolve(item.value)
+ })
+ })
+
+ if (!gitBashPath) {
+ gitBashPath = await new Promise(resolve => {
+ let reg = new Registry({ hive: Registry.HKCU, key: '\\Software\\GitForWindows' })
+ reg.get('InstallPath', (err, item) => {
+ if (err || !item) {
+ resolve(null)
+ return
+ }
+ resolve(item.value)
+ })
+ })
+ }
+
+ if (!gitBashPath) {
+ return []
+ }
+
+ return [{
+ id: 'git-bash',
+ name: 'Git-Bash',
+ command: path.join(gitBashPath, 'bin', 'bash.exe'),
+ args: [ '--login', '-i' ],
+ env: {
+ TERM: 'cygwin',
+ }
+ }]
+ }
+}
diff --git a/terminus-terminal/src/shells/linuxDefault.ts b/terminus-terminal/src/shells/linuxDefault.ts
new file mode 100644
index 00000000..375c1e41
--- /dev/null
+++ b/terminus-terminal/src/shells/linuxDefault.ts
@@ -0,0 +1,40 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform, LogService, Logger } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class LinuxDefaultShellProvider extends ShellProvider {
+ private logger: Logger
+
+ constructor (
+ private hostApp: HostAppService,
+ log: LogService,
+ ) {
+ super()
+ this.logger = log.create('linuxDefaultShell')
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Linux) {
+ return []
+ }
+ let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
+ .split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
+ if (!line) {
+ this.logger.warn('Could not detect user shell')
+ return [{
+ id: 'default',
+ name: 'User default',
+ command: '/bin/sh'
+ }]
+ } else {
+ return [{
+ id: 'default',
+ name: 'User default',
+ command: line.split(':')[6]
+ }]
+ }
+ }
+}
diff --git a/terminus-terminal/src/shells/macDefault.ts b/terminus-terminal/src/shells/macDefault.ts
new file mode 100644
index 00000000..253a8231
--- /dev/null
+++ b/terminus-terminal/src/shells/macDefault.ts
@@ -0,0 +1,26 @@
+import { exec } from 'mz/child_process'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class MacOSDefaultShellProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.macOS) {
+ return []
+ }
+ let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
+ return [{
+ id: 'default',
+ name: 'User default',
+ command: shellEntry.split(' ')[1].trim()
+ }]
+ }
+}
diff --git a/terminus-terminal/src/shells/posix.ts b/terminus-terminal/src/shells/posix.ts
new file mode 100644
index 00000000..c9f858a7
--- /dev/null
+++ b/terminus-terminal/src/shells/posix.ts
@@ -0,0 +1,29 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class POSIXShellsProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform === Platform.Windows) {
+ return []
+ }
+ return (await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
+ .split('\n')
+ .map(x => x.trim())
+ .filter(x => x && !x.startsWith('#'))
+ .map(x => ({
+ id: x,
+ name: x,
+ command: x,
+ }))
+ }
+}
diff --git a/terminus-terminal/src/shells/windowsStock.ts b/terminus-terminal/src/shells/windowsStock.ts
new file mode 100644
index 00000000..dd57339d
--- /dev/null
+++ b/terminus-terminal/src/shells/windowsStock.ts
@@ -0,0 +1,40 @@
+import * as path from 'path'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform, ElectronService } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class WindowsStockShellsProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ private electron: ElectronService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Windows) {
+ return []
+ }
+ return [
+ {
+ id: 'clink',
+ name: 'CMD (clink)',
+ command: 'cmd.exe',
+ args: [
+ '/k',
+ path.join(
+ path.dirname(this.electron.app.getPath('exe')),
+ 'resources',
+ 'clink',
+ `clink_${process.arch}.exe`,
+ ),
+ 'inject',
+ ]
+ },
+ { id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
+ { id: 'powershell', name: 'PowerShell', command: 'powershell.exe' },
+ ]
+ }
+}
diff --git a/terminus-terminal/src/shells/wsl.ts b/terminus-terminal/src/shells/wsl.ts
new file mode 100644
index 00000000..65980f53
--- /dev/null
+++ b/terminus-terminal/src/shells/wsl.ts
@@ -0,0 +1,31 @@
+import * as fs from 'mz/fs'
+import { Injectable } from '@angular/core'
+import { HostAppService, Platform } from 'terminus-core'
+
+import { ShellProvider, IShell } from '../api'
+
+@Injectable()
+export class WSLShellProvider extends ShellProvider {
+ constructor (
+ private hostApp: HostAppService,
+ ) {
+ super()
+ }
+
+ async provide (): Promise {
+ if (this.hostApp.platform !== Platform.Windows) {
+ return []
+ }
+
+ const wslPath = `${process.env.windir}\\system32\\bash.exe`
+ if (!await fs.exists(wslPath)) {
+ return []
+ }
+
+ return [{
+ id: 'wsl',
+ name: 'Bash on Windows',
+ command: wslPath
+ }]
+ }
+}
diff --git a/terminus-terminal/tsconfig.json b/terminus-terminal/tsconfig.json
index 1d6cfcbf..d2383e0e 100644
--- a/terminus-terminal/tsconfig.json
+++ b/terminus-terminal/tsconfig.json
@@ -3,6 +3,10 @@
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "src",
- "declarationDir": "dist"
+ "declarationDir": "dist",
+ "paths": {
+ "terminus-*": ["terminus-*"],
+ "*": ["app/node_modules/*"]
+ }
}
}
diff --git a/terminus-terminal/webpack.config.js b/terminus-terminal/webpack.config.js
index 6eead6e0..52546f03 100644
--- a/terminus-terminal/webpack.config.js
+++ b/terminus-terminal/webpack.config.js
@@ -34,9 +34,17 @@ module.exports = {
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'] },
+ {
+ test: /\.(ttf|eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
+ loader: "url-loader",
+ options: {
+ limit: 999999999999,
+ }
+ },
]
},
externals: [
+ 'electron',
'fs',
'font-manager',
'path',
diff --git a/terminus-terminal/yarn.lock b/terminus-terminal/yarn.lock
new file mode 100644
index 00000000..f145d65a
--- /dev/null
+++ b/terminus-terminal/yarn.lock
@@ -0,0 +1,135 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/deep-equal@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.0.tgz#9ebeaa73d1fc4791f038a5f1440e0449ea968495"
+
+"@types/mz@0.0.31":
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.31.tgz#a4d80c082fefe71e40a7c0f07d1e6555bbbc7b52"
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*", "@types/node@7.0.12":
+ version "7.0.12"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.12.tgz#ae5f67a19c15f752148004db07cbbb372e69efc9"
+
+"@types/webpack-env@1.13.0":
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.0.tgz#3044381647e11ee973c5af2e925323930f691d80"
+
+"@types/winreg@^1.2.30":
+ version "1.2.30"
+ resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.30.tgz#91d6710e536d345b9c9b017c574cf6a8da64c518"
+
+any-promise@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+
+big.js@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
+
+connected-domain@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93"
+
+dataurl@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199"
+
+deep-equal@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+file-loader@^0.11.2:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34"
+ dependencies:
+ loader-utils "^1.0.2"
+
+font-manager@0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/font-manager/-/font-manager-0.2.2.tgz#18a1c5b6ec7f91e22a17c71cbbaa0ea4e68e3a44"
+ dependencies:
+ nan "~2.2.0"
+
+hterm-umdjs@1.1.3:
+ version "1.1.3+1.58.sha.15ed490"
+ resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.1.3.tgz#8b57bcaded5ba9541d6c8e32a82b34abb93e885e"
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+loader-utils@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+mz@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mz/-/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce"
+ dependencies:
+ any-promise "^1.0.0"
+ object-assign "^4.0.1"
+ thenify-all "^1.0.0"
+
+nan@2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8"
+
+nan@~2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.2.1.tgz#d68693f6b34bb41d66bc68b3a4f9defc79d7149b"
+
+node-pty@0.6.8:
+ version "0.6.8"
+ resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.6.8.tgz#a7b145397bef23a719128a75b20d4821726dfe90"
+ dependencies:
+ nan "2.5.0"
+
+object-assign@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+ps-node@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3"
+ dependencies:
+ table-parser "^0.1.3"
+
+runes@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.2.tgz#1ddc1ea41de769cb32fc068a64fbbc45cd21052e"
+
+table-parser@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0"
+ dependencies:
+ connected-domain "^1.0.0"
+
+thenify-all@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+ dependencies:
+ thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
+ dependencies:
+ any-promise "^1.0.0"
+
+winreg@^1.2.3:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"