Compare commits

...

57 Commits

Author SHA1 Message Date
Eugene Pankov
329d0448d3 reconfigure terminals on DPI change (fixes #576) 2019-02-10 00:23:49 +01:00
Eugene Pankov
100436f511 set xterm as default frontend (fixes #542) 2019-02-09 22:40:31 +01:00
Eugene Pankov
22d3e35723 ignore events on destroyed windows 2019-02-09 22:38:45 +01:00
Eugene Pankov
9cdcc8d8e5 fixed #649 2019-02-09 22:10:42 +01:00
Eugene Pankov
168e6f17dc allow selecting ssh ciphers (fixes #645) 2019-02-09 18:52:09 +01:00
Eugene Pankov
a2c636fdbf console logging 2019-02-09 17:44:23 +01:00
Eugene Pankov
413ca70729 Warn when enabling ConPTY on older insider builds (fixes #609, fixes #594) 2019-02-09 17:44:17 +01:00
Eugene Pankov
6f99e6c14b Merge branch 'master' of github.com:Eugeny/terminus 2019-01-30 19:46:25 +01:00
Eugene Pankov
e65811786d bumped node-ssh2 (fixes #605) 2019-01-30 18:11:49 +01:00
Eugene
aecd381b25 Merge pull request #633 from sylveon/master
Update windows-swca dependency
2019-01-30 16:48:11 +01:00
Eugene Pankov
89465f57d5 bumped node-pty 2019-01-30 13:02:35 +01:00
Charles Milette
3bf0ac43ef Update yarn lockfile 2019-01-29 15:37:40 -05:00
Charles Milette
a66dd43e1e Update windows-swca dependency 2019-01-29 15:33:50 -05:00
Eugene Pankov
dd4566cf02 #618 fixes 2019-01-27 23:58:55 +01:00
Eugene Pankov
f2be34d137 limit max font size (fixes #618) 2019-01-27 23:40:33 +01:00
Eugene Pankov
e28c619bdc force en-us locale (#618) 2019-01-27 23:39:05 +01:00
Eugene Pankov
04bf5dbcfb fixed offset with tabs on bottom on macos (fixes #629) 2019-01-27 23:12:46 +01:00
Eugene Pankov
a2128ca1f2 use ssh connection name for the tab's title (fixes #621) 2019-01-27 22:56:50 +01:00
Eugene Pankov
bf0d02d1fc tab duplication (fixes #588) 2019-01-27 22:45:08 +01:00
Eugene Pankov
792de65696 properly recover tabs with xterm 2019-01-27 22:01:55 +01:00
Eugene Pankov
fab21f6859 mention save-output plugin 2019-01-27 22:01:46 +01:00
Eugene
b0b01b98be Update README.md 2019-01-21 15:52:55 +01:00
Eugene
24dff4b5b7 Update README.md 2019-01-18 13:56:08 +00:00
Eugene Pankov
78f8f4005e fixed #610 2019-01-16 17:13:34 +00:00
Eugene Pankov
38cfb3f036 middle click to paste (fixes #613) 2019-01-16 16:46:01 +00:00
Eugene Pankov
4e4d8a0e91 bumped node-pty 2019-01-16 16:25:43 +00:00
Eugene Pankov
21cfd14f1c use the upstream xtermjs 2019-01-16 16:16:06 +00:00
Eugene Pankov
a64bbe145c fixed automatic resizing with xterm 2019-01-16 15:23:55 +00:00
Eugene Pankov
6a5dc79c5d bumped plugin versions 2019-01-10 12:44:12 +01:00
Eugene Pankov
b799128427 fixed TerminalContextMenuProvider typing 2019-01-10 12:44:07 +01:00
Eugene Pankov
8b64a819e7 expose DOM element ref from BaseTerminalTab 2019-01-08 16:37:54 +03:00
Eugene Pankov
5b78a5c1ed made tab context menu extensible 2019-01-07 19:30:03 +03:00
Eugene Pankov
91b318853f replace the stock installer gif (fixes #606) 2019-01-07 17:31:16 +03:00
Eugene Pankov
ce3610c2da automatically recover ssh tabs (fixes #583) 2019-01-06 11:54:26 +01:00
Eugene Pankov
d03430fb2e ssh - show connection log while connecting 2019-01-06 11:14:13 +01:00
Eugene Pankov
caacc01aea split common terminal behaviour into BaseTerminalTab 2019-01-05 16:54:22 +01:00
Eugene Pankov
bcb6963c35 show ssh connection errors 2019-01-05 15:19:02 +01:00
Eugene Pankov
deb99b0865 wrap TerminalTab into SSHTab 2019-01-05 15:17:41 +01:00
Eugene Pankov
2101c18657 fixed saving ssh connections (fixes #436) 2019-01-05 15:03:31 +01:00
Eugene Pankov
1a258f32b0 fixed npm detection when fish is the default shell (#584) 2019-01-05 14:53:19 +01:00
Eugene Pankov
3aaf490f57 fixed #597 2019-01-05 14:51:36 +01:00
Eugene Pankov
9faa346699 better messageboxes 2019-01-03 17:20:02 +03:00
Eugene Pankov
d5b6a686f8 added settings tab icons 2019-01-03 17:19:50 +03:00
Eugene Pankov
492d006f64 xterm scrollback fix 2019-01-03 17:07:38 +03:00
Eugene Pankov
d999320c24 bumped plugin versions 2019-01-03 13:08:57 +03:00
Eugene Pankov
5142d12e7e fixed macos zip artifact naming 2019-01-03 13:01:15 +03:00
Eugene Pankov
453c613571 bumped xterm scrollback size (fixes #589) 2019-01-03 12:55:14 +03:00
Eugene Pankov
ccc34ae4d9 bumped angular to rc 2018-12-30 17:53:07 +01:00
Eugene Pankov
4362b5c50b bumped node-gyp 2018-12-30 17:44:52 +01:00
Eugene Pankov
2d6023446c bumped electron-builder 2018-12-30 17:35:09 +01:00
Eugene Pankov
dcd43dc019 fixed the Preferences menu item 2018-12-30 17:32:30 +01:00
Eugene Pankov
d8e70f9693 bumped nodejs on travis 2018-12-30 17:26:01 +01:00
Eugene Pankov
7a26e8bd65 ignore non-existent CWDs (fixes #586) 2018-12-30 15:59:40 +01:00
Eugene Pankov
d56287587c bumped electron to stable 2018-12-30 15:54:17 +01:00
Eugene Pankov
8793613117 potentially fixed #576 2018-12-29 13:27:45 +01:00
Eugene Pankov
92afec75e7 fixed plugin blacklisting 2018-12-29 12:50:14 +01:00
Eugene Pankov
ca71ec24f8 fixed #585 2018-12-29 12:41:32 +01:00
82 changed files with 1422 additions and 1135 deletions

View File

@@ -6,7 +6,7 @@ matrix:
env: BUILD_FOR=macos
language: node_js
node_js: 8
node_js: 10
cache:
directories:

View File

@@ -6,7 +6,7 @@
</p>
<p align="center">
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://t.me/joinchat/AAAAAEZuCv2WKKYcfyQ3QA">Community</a> | <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts">Latest Windows nightly</a>
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://t.me/joinchat/HgLqPhRg9Inhmm7WD3H1BQ">Community</a> | <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts">Latest Windows nightly</a>
</p>
----
@@ -19,8 +19,9 @@
* Full Unicode support including double-width characters
* Doesn't choke on fast-flowing outputs
* Proper shell-like experience on Windows including tab completion (via Clink)
* PowerShell Core, WSL (Bash on Windows), PowerShell, Git-Bash, Cygwin, Cmder and CMD support
* Tab persistence on macOS and Linux
* PowerShell (+Core), WSL (Bash on Windows), Git-Bash, Cygwin, Cmder and CMD support
* Remembers your tabs
* Integrated SSH client and connection manager
[![Buy me a coffee](https://github.com/Eugeny/terminus/raw/master/docs/kofi.png)](https://ko-fi.com/eugeny)
@@ -38,6 +39,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to terminal tabs
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
---

View File

@@ -1,4 +1,5 @@
import { app, ipcMain, Menu, Tray, shell } from 'electron'
import * as electron from 'electron'
import { loadConfig } from './config'
import { Window, WindowOptions } from './window'
@@ -18,6 +19,11 @@ export class Application {
}
app.commandLine.appendSwitch('disable-http-cache')
app.commandLine.appendSwitch('lang', 'EN')
}
init () {
electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed'))
}
async newWindow (options?: WindowOptions): Promise<Window> {
@@ -103,7 +109,7 @@ export class Application {
{
label: 'Preferences',
accelerator: 'Cmd+,',
async click () {
click: async () => {
if (!this.hasWindows()) {
await this.newWindow()
}

View File

@@ -12,7 +12,6 @@ if (!process.env.TERMINUS_PLUGINS) {
const application = new Application()
ipcMain.on('app:new-window', () => {
console.log('new-window')
application.newWindow()
})
@@ -59,5 +58,6 @@ app.on('ready', () => {
}
]))
}
application.init()
application.newWindow({ hidden: argv.hidden })
})

View File

@@ -1,4 +1,5 @@
import { Subject, Observable } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
import { BrowserWindow, app, ipcMain, Rectangle } from 'electron'
import ElectronConfig = require('electron-config')
import * as os from 'os'
@@ -10,7 +11,7 @@ let AccentState: any
let DwmEnableBlurBehindWindow: any
if (process.platform === 'win32') {
SetWindowCompositionAttribute = require('windows-swca').SetWindowCompositionAttribute
AccentState = require('windows-swca').AccentState
AccentState = require('windows-swca').ACCENT_STATE
DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow
}
@@ -102,16 +103,14 @@ export class Window {
if (process.platform === 'win32') {
if (parseFloat(os.release()) >= 10) {
let attribValue = AccentState.ACCENT_DISABLED
let color = 0x00000000
if (enabled) {
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
attribValue = AccentState.ACCENT_ENABLE_FLUENT
color = 0x01000000 // using a small alpha because acrylic bugs out at full transparency.
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
} else {
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
}
}
SetWindowCompositionAttribute(this.window, attribValue, color)
SetWindowCompositionAttribute(this.window.getNativeWindowHandle(), attribValue, 0x00000000)
} else {
DwmEnableBlurBehindWindow(this.window, enabled)
}
@@ -143,6 +142,16 @@ export class Window {
this.visible.next(false)
})
let moveSubscription = new Observable<void>(observer => {
this.window.on('move', () => observer.next())
}).pipe(debounceTime(250)).subscribe(() => {
this.window.webContents.send('host:window-moved')
})
this.window.on('closed', () => {
moveSubscription.unsubscribe()
})
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
@@ -173,28 +182,28 @@ export class Window {
})
ipcMain.on('window-focus', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.focus()
})
ipcMain.on('window-maximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.maximize()
})
ipcMain.on('window-unmaximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.unmaximize()
})
ipcMain.on('window-toggle-maximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
if (this.window.isMaximized()) {
@@ -205,42 +214,42 @@ export class Window {
})
ipcMain.on('window-minimize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.minimize()
})
ipcMain.on('window-set-bounds', (event, bounds) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setBounds(bounds)
})
ipcMain.on('window-set-always-on-top', (event, flag) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setAlwaysOnTop(flag)
})
ipcMain.on('window-set-vibrancy', (event, enabled, type) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.setVibrancy(enabled, type)
})
ipcMain.on('window-set-title', (event, title) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setTitle(title)
})
ipcMain.on('window-bring-to-front', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
if (this.window.isMinimized()) {
@@ -250,7 +259,10 @@ export class Window {
this.window.moveTop()
})
ipcMain.on('window-close', () => {
ipcMain.on('window-close', event => {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.closing = true
this.window.close()
})

View File

@@ -13,13 +13,13 @@
"watch": "webpack --progress --color --watch"
},
"dependencies": {
"@angular/animations": "7.2.0-beta.1",
"@angular/common": "7.2.0-beta.1",
"@angular/compiler": "7.2.0-beta.1",
"@angular/core": "7.2.0-beta.1",
"@angular/forms": "7.2.0-beta.1",
"@angular/platform-browser": "7.2.0-beta.1",
"@angular/platform-browser-dynamic": "7.2.0-beta.1",
"@angular/animations": "7.2.0-rc.0",
"@angular/common": "7.2.0-rc.0",
"@angular/compiler": "7.2.0-rc.0",
"@angular/core": "7.2.0-rc.0",
"@angular/forms": "7.2.0-rc.0",
"@angular/platform-browser": "7.2.0-rc.0",
"@angular/platform-browser-dynamic": "7.2.0-rc.0",
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
"devtron": "1.4.0",
"electron-config": "0.2.1",
@@ -36,7 +36,7 @@
},
"optionalDependencies": {
"windows-blurbehind": "^1.0.0",
"windows-swca": "^1.1.1"
"windows-swca": "^2.0.1"
},
"devDependencies": {
"@types/mz": "0.0.31"

View File

@@ -15,6 +15,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { getRootModule } from './app.module'
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
;(process as any).enablePromiseAPI = true
if (process.platform === 'win32') {
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
}

View File

@@ -16,11 +16,11 @@ function normalizePath (path: string): string {
nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x)))
if (process.env.DEV) {
if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
}
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const builtinPluginsPath = process.env.TERMINUS_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'),

View File

@@ -9,7 +9,6 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,

View File

@@ -9,7 +9,7 @@ module.exports = {
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
bundle: path.resolve(__dirname, 'src/entry.ts'),
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -7,7 +7,7 @@ module.exports = {
entry: {
main: path.resolve(__dirname, 'lib/index.ts'),
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
context: __dirname,
devtool: 'source-map',
output: {

View File

@@ -2,52 +2,52 @@
# yarn lockfile v1
"@angular/animations@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-beta.1.tgz#c288de4a89b0197ba53a8411173ec4085fbbd9c9"
integrity sha512-82u9L2poaREjkTJlYEKdOO1B1LaVBwqJBZvIXU04+21WQBJoi050sxUl6lmjVVs5rRc0e/y2gifyrb42pUEntA==
"@angular/animations@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-rc.0.tgz#12849f8ab104d309ec99c0ceb170a895c15d3d44"
integrity sha512-CRQNQ6QVTuf4nCHVLVpKQx7YPpNPfnTF79KVWzHefkkyS3URRuEgvE4jCED4oTJ4BEsmkjXyt51VeDV0FgqQFg==
dependencies:
tslib "^1.9.0"
"@angular/common@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-beta.1.tgz#be9d14f239b7a390fc056e3bb540da181631fb1a"
integrity sha512-+aYfO20nrqurOLxM0w/UJkOHjGzNFOc2j52ggyj1vr62nTk+W63j4P8tcUsW6iHFCsOF5auSkclKUbNIliMf0A==
"@angular/common@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-rc.0.tgz#60d3540c6cdcf3440f67e2c15cf8f1c7b1160d9d"
integrity sha512-Xv60KEP1kpF74kpN1xtps0W++PUXLUMK/0tDblUZH7tBWvS0XwEwtuK5B6wcs+I5nqZkPgvlvOyiVZvOLraWOg==
dependencies:
tslib "^1.9.0"
"@angular/compiler@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-beta.1.tgz#355a10a80615afdd1d6a5ed682222e9e57e65fd7"
integrity sha512-KxI93dLm1SPNbazfG41QcmxVS7T9VmQ8wzhMHOVJo4DP77g2E5xUB5nOInMCMI43lbZEIckBxo/ci4jwiiq8uA==
"@angular/compiler@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-rc.0.tgz#603dbec25d6c2beea08a293c68c39b40e2ea81e2"
integrity sha512-tvgGJx0urSz/qn6upmcjX3N3dyWQ9m5mQOwJxmN4qekxjOtSRml5yt2KtlaUTkGsjkEmEVfSHel+X1TwzBdhYw==
dependencies:
tslib "^1.9.0"
"@angular/core@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-beta.1.tgz#32bef8f3d424333791d0e0bd4a3f3afee9b1349d"
integrity sha512-gJzQAauezAMU8vEEMh1PrXv52fA9ceUXac/tJ8KIi08zEjyIRXLvKggWr7YXAbt5LwgKsn27JwecqeS5h4K/BQ==
"@angular/core@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-rc.0.tgz#57c0e26130288c3b58466f079828c028bdf6221f"
integrity sha512-2u11TNlLorw3JhuczCPwl8UmxE+ja2Q/ghBl8iYi4SIBWiBO1K0wVT13Ts7eojk63yZcg60lyYYCegXBmHLTuw==
dependencies:
tslib "^1.9.0"
"@angular/forms@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-beta.1.tgz#399ea4585502027d53396eac935f8f20bd5ad2bf"
integrity sha512-1NQ2hw8Y4hjgr5qXoOvQJRjmQno/fhQUuIRx0SC7hYySur1E9vcI8rZDVDB+LwiGexALh8H70zwJ64lNxzwpvA==
"@angular/forms@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-rc.0.tgz#4eb473018084bb81be3e2e1ae8afa8d2b2117a6c"
integrity sha512-OWP1zzYQiuqtoltdlhkcVjHxg78exbt7z1lr8RSjybr/Snc5zSFhnZF6byasd/4lzVySuujsMXkTK7D8x6hedA==
dependencies:
tslib "^1.9.0"
"@angular/platform-browser-dynamic@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-beta.1.tgz#4a95ee6d53fb02e529f1f1842ee8839a25ec4790"
integrity sha512-wnf6WSfh9AsBzI8I//eNolmQ2rwMIk/KIuGmTEOAuAxRMLgqzZUA3PjX2XAE6oUUowqy1MET1UFiqjDf/NZcNQ==
"@angular/platform-browser-dynamic@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-rc.0.tgz#5ea47d094c53a0ba34ecbb0dfdcef452fa05dc9a"
integrity sha512-uqT27oh9m58L6MUjgvT+7NpAFbigOnnTUWMsCLijNUKd7i37T6UxTVKPvuqNHlaLXsmDRxVHN3INI0IrWZ3R+w==
dependencies:
tslib "^1.9.0"
"@angular/platform-browser@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-beta.1.tgz#20c272ebfcccb3151baed7ceddbafdb359f54cd8"
integrity sha512-sCMzCFCdE4Dq9foXf2PpWPegtVfirHhg+DQzoiwFDYj5i+QB9nWY7BOLlrCPAWpd8opUxCsaLrzXbfgM40FAGA==
"@angular/platform-browser@7.2.0-rc.0":
version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-rc.0.tgz#c6d1f0b2328b1d81649bea70c23edc33de729015"
integrity sha512-r0ak7SVLWrivd4S0MXWmqNLeF6NNOBAopnjrhUu2j5I00u7/QfLrX0E5zRlJ8JkARVjer6Wm+D1ztlOWw5jHag==
dependencies:
tslib "^1.9.0"
@@ -70,6 +70,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
integrity sha512-Y3EAG7VA7NVNbZek/fjJtILnmTk/ZfpJuWZGDBqDZ1dVIxgJJJ82fXPW7pKnqyV9CD/9bcPOCi7eErUqGMHOrA==
"@types/node@^10.12.18":
version "10.12.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
accessibility-developer-tools@^2.11.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
@@ -620,10 +625,12 @@ windows-blurbehind@^1.0.0:
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.0.tgz#050efb988704c44335bdc3efefd757f6e463b8ac"
integrity sha512-lO+A7fhTHO7oy9zJM3o1AdzfSQrmtPkdwvleeuww840ghijjEA1f1Zp8bKA3mJu2DFNtVT40fwmqtgsCGat4UA==
windows-swca@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-1.1.1.tgz#0b3530278c67d408baac71c3a6aeb16d55318bf8"
integrity sha512-hKmHrNYJD72Kg0u35fjkiFIuMKuC+Tztmf3Obnf4aTkNjstEpbSEspEeSo3ZNixaVCETA1dLbDkVUQVF1QxtWA==
windows-swca@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-2.0.1.tgz#25d78ce25251292061494a0ad07c02282b28b4e3"
integrity sha512-flj+HD6RUemZUvKKguLLnMUOYkQSgDu9qrhUIO4cydvtb/x+sxU8XmpZUtugYuydcdikB9zsCOMgKnAqIQ+7nw==
dependencies:
"@types/node" "^10.12.18"
wrap-ansi@^2.0.0:
version "2.1.0"

BIN
build/windows/squirrel.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -14,8 +14,8 @@
"core-js": "2.4.1",
"cross-env": "4.0.0",
"css-loader": "0.28.0",
"electron": "4.0.0-beta.7",
"electron-builder": "^20.38.2",
"electron": "4.0.0",
"electron-builder": "^20.38.4",
"electron-builder-squirrel-windows": "^20.28.3",
"electron-installer-snap": "^3.0.0",
"electron-rebuild": "^1.8.2",
@@ -24,7 +24,7 @@
"html-loader": "0.4.4",
"json-loader": "0.5.4",
"node-abi": "^2.4.4",
"node-gyp": "^3.6.2",
"node-gyp": "^3.8.0",
"node-sass": "^4.5.3",
"npmlog": "4.1.0",
"npx": "^10.2.0",
@@ -53,6 +53,9 @@
"yaml-loader": "0.4.0",
"yarn": "^1.10.1"
},
"resolutions": {
"*/node-abi": "^2.5.0"
},
"build": {
"appId": "org.terminus",
"productName": "Terminus",
@@ -74,6 +77,7 @@
},
"squirrelWindows": {
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
"loadingGif": "./build/windows/squirrel.gif",
"artifactName": "terminus-${version}-setup.exe"
},
"portable": {
@@ -82,6 +86,7 @@
"mac": {
"category": "public.app-category.video",
"icon": "./build/mac/icon.icns",
"artifactName": "terminus-${version}-macos.${ext}",
"publish": [
"github"
],
@@ -122,9 +127,9 @@
},
"scripts": {
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
"watch": "cross-env DEV=1 webpack --progress --color --watch",
"start": "cross-env DEV=1 electron app --debug",
"prod": "cross-env DEV=1 electron app",
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
"prod": "cross-env TERMINUS_DEV=1 electron app",
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
"postinstall": "node ./scripts/install-deps.js"
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-community-color-schemes",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "Community color schemes for Terminus",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-community-color-schemes:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-core",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "Terminus core",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -4,6 +4,7 @@ export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider'
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
export { Theme } from './theme'
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
export { AppService } from '../services/app.service'
export { ConfigService } from '../services/config.service'

View File

@@ -0,0 +1,8 @@
import { BaseTabComponent } from '../components/baseTab.component'
import { TabHeaderComponent } from '../components/tabHeader.component'
export abstract class TabContextMenuItemProvider {
weight = 0
abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]>
}

View File

@@ -9,7 +9,6 @@ import { HotkeysService } from '../services/hotkeys.service'
import { Logger, LogService } from '../services/log.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 { UpdaterService } from '../services/updater.service'
import { TouchbarService } from '../services/touchbar.service'
@@ -69,7 +68,6 @@ export class AppRootComponent {
constructor (
private docking: DockingService,
private electron: ElectronService,
private tabRecovery: TabRecoveryService,
private hotkeys: HotkeysService,
private updater: UpdaterService,
private touchbar: TouchbarService,
@@ -199,9 +197,7 @@ export class AppRootComponent {
}
async ngOnInit () {
await this.tabRecovery.recoverTabs()
this.ready = true
this.tabRecovery.saveTabs(this.app.tabs)
this.app.emitReady()
}

View File

@@ -20,6 +20,10 @@
flex-direction: row;
align-items: center;
.off {
color: rgba(0, 0, 0, .5);
}
.icon {
position: relative;
flex: none;

View File

@@ -1,6 +1,7 @@
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
import { SortableComponent } from 'ng2-dnd'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component'
import { HotkeysService } from '../services/hotkeys.service'
@@ -8,16 +9,6 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
@Component({
selector: 'tab-header',
template: require('./tabHeader.component.pug'),
@@ -31,16 +22,14 @@ export class TabHeaderComponent {
@Input() progress: number
@ViewChild('handle') handle: ElementRef
private completionNotificationEnabled = false
constructor (
public app: AppService,
private electron: ElectronService,
private zone: NgZone,
private hostApp: HostAppService,
private ngbModal: NgbModal,
private hotkeys: HotkeysService,
private parentDraggable: SortableComponent,
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
) {
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
if (this.app.activeTab === this.tab) {
@@ -49,6 +38,7 @@ export class TabHeaderComponent {
}
}
})
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
}
ngOnInit () {
@@ -69,6 +59,15 @@ export class TabHeaderComponent {
}).catch(() => null)
}
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = []
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
items.push({ type: 'separator' })
items = items.concat(section)
}
return items.slice(1)
}
@HostListener('dblclick') onDoubleClick (): void {
this.showRenameTabModal()
}
@@ -80,96 +79,7 @@ export class TabHeaderComponent {
if ($event.which === 3) {
event.preventDefault()
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
{
label: 'Close',
click: () => this.zone.run(() => {
this.app.closeTab(this.tab, true)
})
},
{
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Rename',
click: () => this.zone.run(() => this.showRenameTabModal())
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === this.tab.color).name,
submenu: COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: this.tab.color === color.value,
click: () => this.zone.run(() => {
this.tab.color = color.value
}),
})),
}
])
if ((this.tab as any).saveAsProfile) {
contextMenu.append(new this.electron.MenuItem({
label: 'Save as a profile',
click: () => this.zone.run(() => (this.tab as any).saveAsProfile())
}))
}
let process = await this.tab.getCurrentProcess()
if (process) {
contextMenu.append(new this.electron.MenuItem({
id: 'sep',
type: 'separator',
}))
contextMenu.append(new this.electron.MenuItem({
id: 'process-name',
enabled: false,
label: 'Current process: ' + process.name,
}))
contextMenu.append(new this.electron.MenuItem({
id: 'completion',
label: 'Notify when done',
type: 'checkbox',
checked: this.completionNotificationEnabled,
click: () => this.zone.run(() => {
this.completionNotificationEnabled = !this.completionNotificationEnabled
if (this.completionNotificationEnabled) {
this.app.observeTabCompletion(this.tab).subscribe(() => {
new Notification('Process completed', {
body: process.name,
}).addEventListener('click', () => {
this.app.selectTab(this.tab)
})
this.completionNotificationEnabled = false
})
} else {
this.app.stopObservingTabCompletion(this.tab)
}
})
}))
}
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
contextMenu.popup({
x: $event.pageX,

View File

@@ -24,9 +24,11 @@ import { AutofocusDirective } from './directives/autofocus.directive'
import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider'
import { Theme } from './api/theme'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config'
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css'
@@ -37,6 +39,9 @@ const PROVIDERS = [
{ provide: Theme, useClass: StandardCompactTheme, multi: true },
{ provide: Theme, useClass: PaperTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
]

View File

@@ -5,6 +5,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from './log.service'
import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
import { TabRecoveryService } from './tabRecovery.service'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@@ -61,11 +62,25 @@ export class AppService {
private config: ConfigService,
private hostApp: HostAppService,
private injector: Injector,
private tabRecovery: TabRecoveryService,
log: LogService,
) {
this.logger = log.create('app')
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
this.tabRecovery.recoverTabs().then(tabs => {
for (let tab of tabs) {
this.openNewTab(tab.type, tab.options)
}
this.tabsChanged$.subscribe(() => {
tabRecovery.saveTabs(this.tabs)
})
setInterval(() => {
tabRecovery.saveTabs(this.tabs)
}, 30000)
})
}
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
@@ -105,7 +120,9 @@ export class AppService {
this.activeTab = tab
this.activeTabChange.next(tab)
if (this.activeTab) {
this.activeTab.emitFocused()
setImmediate(() => {
this.activeTab.emitFocused()
})
this.hostApp.setTitle(this.activeTab.title)
}
}
@@ -160,6 +177,17 @@ export class AppService {
this.tabClosed.next(tab)
}
async duplicateTab (tab: BaseTabComponent) {
let token = await tab.getRecoveryToken()
if (!token) {
return
}
let recoveredTab = await this.tabRecovery.recoverTab(token)
if (recoveredTab) {
this.openNewTab(recoveredTab.type, recoveredTab.options)
}
}
async closeWindow () {
for (let tab of this.tabs) {
if (!await tab.canClose()) {

View File

@@ -1,6 +1,11 @@
import { Injectable } from '@angular/core'
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
export interface MessageBoxResponse {
response: number
checkboxChecked?: boolean
}
@Injectable({ providedIn: 'root' })
export class ElectronService {
app: any
@@ -54,4 +59,15 @@ export class ElectronService {
this.remote.Menu.sendActionToFirstResponder('hide:')
}
}
showMessageBox (
browserWindow: Electron.BrowserWindow,
options: Electron.MessageBoxOptions
): Promise<MessageBoxResponse> {
return new Promise(resolve => {
this.dialog.showMessageBox(browserWindow, options, (response, checkboxChecked) => {
resolve({ response, checkboxChecked })
})
})
}
}

View File

@@ -30,6 +30,8 @@ export class HostAppService {
private cliOpenProfile = new Subject<string>()
private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>()
private windowMoved = new Subject<void>()
private displayMetricsChanged = new Subject<void>()
private logger: Logger
private windowId: number
@@ -41,6 +43,8 @@ export class HostAppService {
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
get windowMoved$ (): Observable<void> { return this.windowMoved }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
constructor (
private zone: NgZone,
@@ -80,6 +84,14 @@ export class HostAppService {
this.zone.run(() => this.windowCloseRequest.next())
})
electron.ipcRenderer.on('host:window-moved', () => {
this.zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next())
})
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => {
this.logger.info('Second instance', argv)
const op = argv._[0]

View File

@@ -23,7 +23,7 @@ const initializeWinston = (electron: ElectronService) => {
colorize: false
}),
new winston.transports.Console({
level: 'info',
level: 'debug',
handleExceptions: false,
json: false,
colorize: true

View File

@@ -2,7 +2,6 @@ import { Injectable, Inject } from '@angular/core'
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'
import { ConfigService } from '../services/config.service'
@Injectable({ providedIn: 'root' })
@@ -11,17 +10,10 @@ export class TabRecoveryService {
constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private app: AppService,
private config: ConfigService,
log: LogService
) {
this.logger = log.create('tabRecovery')
app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs)
})
setInterval(() => {
this.saveTabs(app.tabs)
}, 30000)
}
async saveTabs (tabs: BaseTabComponent[]) {
@@ -34,25 +26,32 @@ export class TabRecoveryService {
)
}
async recoverTabs (): Promise<void> {
async recoverTab (token: any): Promise<RecoveredTab> {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try {
let tab = await provider.recover(token)
if (tab) {
return tab
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
return null
}
async recoverTabs (): Promise<RecoveredTab[]> {
if (window.localStorage.tabsRecovery) {
let tabs: RecoveredTab[] = []
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try {
let tab = await provider.recover(token)
if (tab) {
tabs.push(tab)
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
let tab = await this.recoverTab(token)
if (tab) {
tabs.push(tab)
}
}
tabs.forEach(tab => {
this.app.openNewTab(tab.type, tab.options)
})
return tabs
}
return []
}
}

View File

@@ -0,0 +1,144 @@
import { Injectable, NgZone } from '@angular/core'
import { AppService } from './services/app.service'
import { BaseTabComponent } from './components/baseTab.component'
import { TabHeaderComponent } from './components/tabHeader.component'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
@Injectable()
export class CloseContextMenu extends TabContextMenuItemProvider {
weight = -5
constructor (
private app: AppService,
private zone: NgZone,
) {
super()
}
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Close',
click: () => this.zone.run(() => {
this.app.closeTab(tab, true)
})
},
{
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.filter(x => x !== tab)) {
this.app.closeTab(t, true)
}
})
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
this.app.closeTab(t, true)
}
})
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
this.app.closeTab(t, true)
}
})
},
]
}
}
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
@Injectable()
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
weight = -1
constructor (
private zone: NgZone,
private app: AppService,
) {
super()
}
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Rename',
click: () => this.zone.run(() => tabHeader.showRenameTabModal())
},
{
label: 'Duplicate',
click: () => this.zone.run(() => this.app.duplicateTab(tab))
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === tab.color).name,
submenu: COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: tab.color === color.value,
click: () => this.zone.run(() => {
tab.color = color.value
}),
})) as Electron.MenuItemConstructorOptions[],
}
]
}
}
@Injectable()
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
constructor (
private app: AppService,
private zone: NgZone,
) {
super()
}
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let process = await tab.getCurrentProcess()
if (process) {
return [
{
id: 'process-name',
enabled: false,
label: 'Current process: ' + process.name,
},
{
label: 'Notify when done',
type: 'checkbox',
checked: (tab as any).__completionNotificationEnabled,
click: () => this.zone.run(() => {
;(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
if ((tab as any).__completionNotificationEnabled) {
this.app.observeTabCompletion(tab).subscribe(() => {
new Notification('Process completed', {
body: process.name,
}).addEventListener('click', () => {
this.app.selectTab(tab)
})
;(tab as any).__completionNotificationEnabled = false
})
} else {
this.app.stopObservingTabCompletion(tab)
}
})
},
]
}
return []
}
}

View File

@@ -5,19 +5,19 @@ import { Theme } from './api'
export class StandardTheme extends Theme {
name = 'Standard'
css = require('./theme.scss')
terminalBackground = '#1D272D'
terminalBackground = '#222a33'
}
@Injectable()
export class StandardCompactTheme extends Theme {
name = 'Compact'
css = require('./theme.compact.scss')
terminalBackground = '#1D272D'
terminalBackground = '#222a33'
}
@Injectable()
export class PaperTheme extends Theme {
name = 'Paper'
css = require('./theme.paper.scss')
terminalBackground = '#1D272D'
terminalBackground = '#f7f1e0'
}

View File

@@ -14,7 +14,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-core:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-plugin-manager",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "Terminus' plugin manager",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -65,7 +65,7 @@ div(*ngIf='npmInstalled')
.input-group.mb-3
.input-group-prepend
.input-group-text
i.fas.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fas.fa-fw.fa-search(*ngIf='availablePluginsReady')
input.form-control(
type='text',

View File

@@ -117,7 +117,7 @@ export class PluginsSettingsTabComponent {
}
disablePlugin (plugin: IPluginInfo) {
this.config.store.pluginBlacklist.push(plugin.name)
this.config.store.pluginBlacklist = [...this.config.store.pluginBlacklist, plugin.name]
this.config.save()
this.config.requestRestart()
}

View File

@@ -48,7 +48,7 @@ export class PluginManagerService {
return
}
if (this.hostApp.platform !== Platform.Windows) {
this.envPath = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim()
this.envPath = (await exec('$SHELL -i -c \'echo $PATH\''))[0].toString().trim()
let searchPaths = this.envPath.split(':')
for (let searchPath of searchPaths) {
if (await fs.exists(path.join(searchPath, 'npm'))) {

View File

@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-plugin-manager:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-settings",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "Terminus terminal settings page",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -1,5 +1,6 @@
export abstract class SettingsTabProvider {
id: string
icon: string
title: string
getComponentType (): any {

View File

@@ -3,6 +3,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='application')
ng-template(ngbTabTitle)
i.fas.fa-fw.fa-window-maximize.mr-2
| Application
ng-template(ngbTabContent)
.d-flex.align-items-center.mb-4
@@ -247,6 +248,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='hotkeys')
ng-template(ngbTabTitle)
i.fas.fa-fw.fa-keyboard.mr-2
| Hotkeys
ng-template(ngbTabContent)
h3.mb-3 Hotkeys
@@ -274,6 +276,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
ng-template(ngbTabTitle)
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
| {{provider.title}}
ng-template(ngbTabContent)
settings-tab-body([provider]='provider')
@@ -281,6 +284,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='config-file')
ng-template(ngbTabTitle)
i.fas.fa-fw.fa-code.mr-2
| Config file
ng-template.test(ngbTabContent)
.d-flex.flex-column.w-100.h-100

View File

@@ -6,7 +6,7 @@ import { SettingsTabComponent } from './components/settingsTab.component'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<RecoveredTab> {
if (recoveryToken.type === 'app:settings') {
if (recoveryToken && recoveryToken.type === 'app:settings') {
return { type: SettingsTabComponent }
}
return null

View File

@@ -14,7 +14,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-settings:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-ssh",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "SSH connection manager for Terminus",
"keywords": [
"terminus-builtin-plugin"
@@ -36,10 +36,11 @@
},
"optionalDependencies": {
"wincredmgr": "^2.0.0",
"xkeychain": "^0.0.6",
"windows-process-tree": "^0.2.3"
"windows-process-tree": "^0.2.3",
"xkeychain": "^0.0.6"
},
"dependencies": {
"ssh2": "^0.5.5"
"ssh2": "^0.8.2",
"ssh2-streams": "^0.4.2"
}
}

View File

@@ -7,6 +7,13 @@ export interface LoginScript {
optional?: boolean
}
export enum SSHAlgorithmType {
HMAC = 'hmac',
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey'
}
export interface SSHConnection {
name?: string
host: string
@@ -19,14 +26,17 @@ export interface SSHConnection {
keepaliveInterval?: number
keepaliveCountMax?: number
readyTimeout?: number
algorithms?: {[t: string]: string[]}
}
export class SSHSession extends BaseSession {
scripts?: LoginScript[]
shell: any
constructor (private shell: any, conn: SSHConnection) {
constructor (public connection: SSHConnection) {
super()
this.scripts = conn.scripts || []
this.scripts = connection.scripts || []
}
start () {
@@ -87,15 +97,21 @@ export class SSHSession extends BaseSession {
}
resize (columns, rows) {
this.shell.setWindow(rows, columns)
if (this.shell) {
this.shell.setWindow(rows, columns)
}
}
write (data) {
this.shell.write(data)
if (this.shell) {
this.shell.write(data)
}
}
kill (signal?: string) {
this.shell.signal(signal || 'TERM')
if (this.shell) {
this.shell.signal(signal || 'TERM')
}
}
async getChildProcesses (): Promise<any[]> {

View File

@@ -85,6 +85,27 @@
placeholder='20000',
[(ngModel)]='connection.readyTimeout',
)
.form-group
label Ciphers
div(*ngFor='let alg of supportedAlgorithms.cipher')
checkbox([text]='alg', [(ngModel)]='algorithms.cipher[alg]')
.form-group
label Key exchange
div(*ngFor='let alg of supportedAlgorithms.kex')
checkbox([text]='alg', [(ngModel)]='algorithms.kex[alg]')
.form-group
label HMAC
div(*ngFor='let alg of supportedAlgorithms.hmac')
checkbox([text]='alg', [(ngModel)]='algorithms.hmac[alg]')
.form-group
label Host key
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
ngb-tab(id='scripts')
ng-template(ngbTabTitle)

View File

@@ -2,7 +2,8 @@ import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ElectronService, HostAppService } from 'terminus-core'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { SSHConnection, LoginScript } from '../api'
import { SSHConnection, LoginScript, SSHAlgorithmType } from '../api'
import { ALGORITHMS } from 'ssh2-streams/lib/constants'
@Component({
template: require('./editConnectionModal.component.pug'),
@@ -12,6 +13,10 @@ export class EditConnectionModalComponent {
newScript: LoginScript
hasSavedPassword: boolean
supportedAlgorithms: {[id: string]: string[]} = {}
defaultAlgorithms: {[id: string]: string[]} = {}
algorithms: {[id: string]: {[a: string]: boolean}} = {}
constructor (
private modalInstance: NgbActiveModal,
private electron: ElectronService,
@@ -19,10 +24,41 @@ export class EditConnectionModalComponent {
private passwordStorage: PasswordStorageService,
) {
this.newScript = { expect: '', send: '' }
for (let k of Object.values(SSHAlgorithmType)) {
this.supportedAlgorithms[k] = ALGORITHMS[
{
[SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
[SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
[SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
[SSHAlgorithmType.HMAC]: 'SUPPORTED_HMAC',
}[k]
]
this.defaultAlgorithms[k] = ALGORITHMS[
{
[SSHAlgorithmType.KEX]: 'KEX',
[SSHAlgorithmType.HOSTKEY]: 'SERVER_HOST_KEY',
[SSHAlgorithmType.CIPHER]: 'CIPHER',
[SSHAlgorithmType.HMAC]: 'HMAC',
}[k]
]
}
console.log(this)
}
async ngOnInit () {
this.hasSavedPassword = !!(await this.passwordStorage.loadPassword(this.connection))
this.connection.algorithms = this.connection.algorithms || {}
for (let k of Object.values(SSHAlgorithmType)) {
if (!this.connection.algorithms[k]) {
this.connection.algorithms[k] = this.defaultAlgorithms[k]
}
this.algorithms[k] = {}
for (let alg of this.connection.algorithms[k]) {
this.algorithms[k][alg] = true
}
}
}
clearSavedPassword () {
@@ -43,6 +79,11 @@ export class EditConnectionModalComponent {
}
save () {
for (let k of Object.values(SSHAlgorithmType)) {
this.connection.algorithms[k] = Object.entries(this.algorithms[k])
.filter(([k, v]) => !!v)
.map(([k, v]) => k)
}
this.modalInstance.close(this.connection)
}
@@ -66,8 +107,17 @@ export class EditConnectionModalComponent {
}
}
deleteScript (script: LoginScript) {
if (confirm(`Delete?`)) {
async deleteScript (script: LoginScript) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: 'Delete this script?',
detail: script.expect,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
this.connection.scripts = this.connection.scripts.filter(x => x !== script)
}
}

View File

@@ -61,7 +61,7 @@ export class SSHModalComponent {
connect (connection: SSHConnection) {
this.close()
this.ssh.connect(connection).catch(error => {
this.ssh.openTab(connection).catch(error => {
this.toastr.error(`Could not connect: ${error}`)
}).then(() => {
setTimeout(() => {

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService } from 'terminus-core'
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
import { SSHConnection, ISSHConnectionGroup } from '../api'
import { EditConnectionModalComponent } from './editConnectionModal.component'
import { PromptModalComponent } from './promptModal.component'
@@ -15,6 +15,8 @@ export class SSHSettingsTabComponent {
constructor (
public config: ConfigService,
private electron: ElectronService,
private hostApp: HostAppService,
private ngbModal: NgbModal,
) {
this.connections = this.config.store.ssh.connections
@@ -44,13 +46,22 @@ export class SSHSettingsTabComponent {
modal.componentInstance.connection = Object.assign({}, connection)
modal.result.then(result => {
Object.assign(connection, result)
this.config.store.ssh.connections = this.connections
this.config.save()
this.refresh()
})
}
deleteConnection (connection: SSHConnection) {
if (confirm(`Delete "${connection.name}"?`)) {
async deleteConnection (connection: SSHConnection) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `Delete "${connection.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
this.connections = this.connections.filter(x => x !== connection)
this.config.store.ssh.connections = this.connections
this.config.save()
@@ -67,14 +78,23 @@ export class SSHSettingsTabComponent {
for (let connection of this.connections.filter(x => x.group === group.name)) {
connection.group = result
}
this.config.store.ssh.connections = this.connections
this.config.save()
this.refresh()
}
})
}
deleteGroup (group: ISSHConnectionGroup) {
if (confirm(`Delete "${group}"?`)) {
async deleteGroup (group: ISSHConnectionGroup) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `Delete "${group}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
for (let connection of this.connections.filter(x => x.group === group.name)) {
connection.group = null
}
@@ -84,6 +104,7 @@ export class SSHSettingsTabComponent {
}
refresh () {
this.connections = this.config.store.ssh.connections
this.childGroups = []
for (let connection of this.connections) {

View File

@@ -0,0 +1,13 @@
:host {
flex: auto;
display: flex;
overflow: hidden;
&> .content {
flex: auto;
position: relative;
display: block;
overflow: hidden;
margin: 15px;
}
}

View File

@@ -0,0 +1,67 @@
import { Component } from '@angular/core'
import { first } from 'rxjs/operators'
import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHConnection, SSHSession } from '../api'
@Component({
template: `
<div
#content
class="content"
></div>
`,
styles: [require('./sshTab.component.scss')],
})
export class SSHTabComponent extends BaseTerminalTabComponent {
connection: SSHConnection
ssh: SSHService
session: SSHSession
ngOnInit () {
this.logger = this.log.create('terminalTab')
this.ssh = this.injector.get(SSHService)
this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession()
})
super.ngOnInit()
setImmediate(() => {
this.setTitle(this.connection.name)
})
}
async initializeSession () {
if (!this.connection) {
this.logger.error('No SSH connection info supplied')
return
}
this.session = new SSHSession(this.connection)
this.attachSessionHandlers()
this.write(`Connecting to ${this.connection.host}`)
let interval = setInterval(() => this.write('.'), 500)
try {
await this.ssh.connectSession(this.session, message => {
this.write('\r\n' + message)
})
} catch (e) {
this.write('\r\n')
this.write(e.message)
return
} finally {
clearInterval(interval)
this.write('\r\n')
}
this.session.resize(this.size.columns, this.size.rows)
this.session.start()
}
async getRecoveryToken (): Promise<any> {
return {
type: 'app:ssh-tab',
connection: this.connection,
}
}
}

View File

@@ -3,17 +3,19 @@ import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
import { SSHModalComponent } from './components/sshModal.component'
import { PromptModalComponent } from './components/promptModal.component'
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
import { SSHTabComponent } from './components/sshTab.component'
import { ButtonProvider } from './buttonProvider'
import { SSHConfigProvider } from './config'
import { SSHSettingsTabProvider } from './settings'
import { RecoveryProvider } from './recoveryProvider'
@NgModule({
imports: [
@@ -27,18 +29,21 @@ import { SSHSettingsTabProvider } from './settings'
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
],
entryComponents: [
EditConnectionModalComponent,
PromptModalComponent,
SSHModalComponent,
SSHSettingsTabComponent,
SSHTabComponent,
],
declarations: [
EditConnectionModalComponent,
PromptModalComponent,
SSHModalComponent,
SSHSettingsTabComponent,
SSHTabComponent,
],
})
export default class SSHModule { }

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
import { SSHTabComponent } from './components/sshTab.component'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<RecoveredTab> {
if (recoveryToken && recoveryToken.type === 'app:ssh-tab') {
return {
type: SSHTabComponent,
options: { connection: recoveryToken.connection },
}
}
return null
}
}

View File

@@ -5,9 +5,9 @@ import * as fs from 'mz/fs'
import * as path from 'path'
import { ToastrService } from 'ngx-toastr'
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
import { TerminalTabComponent } from 'terminus-terminal'
import { SSHConnection, SSHSession } from '../api'
import { PromptModalComponent } from '../components/promptModal.component'
import { SSHTabComponent } from '../components/sshTab.component'
import { PasswordStorageService } from './passwordStorage.service'
const { SSH2Stream } = require('ssh2-streams')
@@ -33,14 +33,31 @@ export class SSHService {
this.logger = log.create('ssh')
}
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
return this.zone.run(() => this.app.openNewTab(
SSHTabComponent,
{ connection }
) as SSHTabComponent)
}
async connectSession (session: SSHSession, logCallback?: (s: string) => void): Promise<void> {
let privateKey: string = null
let privateKeyPassphrase: string = null
let privateKeyPath = connection.privateKey
let privateKeyPath = session.connection.privateKey
if (!logCallback) {
logCallback = (s) => null
}
const log = s => {
logCallback(s)
this.logger.info(s)
}
if (!privateKeyPath) {
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
if (await fs.exists(userKeyPath)) {
this.logger.info('Using user\'s default private key:', userKeyPath)
log(`Using user's default private key: ${userKeyPath}`)
privateKeyPath = userKeyPath
}
}
@@ -49,11 +66,12 @@ export class SSHService {
try {
privateKey = (await fs.readFile(privateKeyPath)).toString()
} catch (error) {
log('Could not read the private key file')
this.toastr.warning('Could not read the private key file')
}
if (privateKey) {
this.logger.info('Loaded private key from', privateKeyPath)
log(`Loading private key from ${privateKeyPath}`)
let encrypted = privateKey.includes('ENCRYPTED')
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
@@ -61,6 +79,7 @@ export class SSHService {
}
if (encrypted) {
let modal = this.ngbModal.open(PromptModalComponent)
log('Key requires passphrase')
modal.componentInstance.prompt = 'Private key passphrase'
modal.componentInstance.password = true
try {
@@ -77,12 +96,12 @@ export class SSHService {
ssh.on('ready', () => {
connected = true
if (savedPassword) {
this.passwordStorage.savePassword(connection, savedPassword)
this.passwordStorage.savePassword(session.connection, savedPassword)
}
this.zone.run(resolve)
})
ssh.on('error', error => {
this.passwordStorage.deletePassword(connection)
this.passwordStorage.deletePassword(session.connection)
this.zone.run(() => {
if (connected) {
this.toastr.error(error.toString())
@@ -92,7 +111,8 @@ export class SSHService {
})
})
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
console.log(name, instructions, instructionsLang)
log(`Keyboard-interactive auth requested: ${name}`)
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
let results = []
for (let prompt of prompts) {
let modal = this.ngbModal.open(PromptModalComponent)
@@ -103,6 +123,14 @@ export class SSHService {
finish(results)
}))
ssh.on('greeting', greeting => {
log('Greeting: ' + greeting)
})
ssh.on('banner', banner => {
log('Banner: ' + banner)
})
let agent: string = null
if (this.hostApp.platform === Platform.Windows) {
let pageantRunning = new Promise<boolean>(resolve => {
@@ -117,48 +145,63 @@ export class SSHService {
agent = process.env.SSH_AUTH_SOCK
}
ssh.connect({
host: connection.host,
port: connection.port || 22,
username: connection.user,
password: connection.privateKey ? undefined : '',
privateKey,
passphrase: privateKeyPassphrase,
tryKeyboard: true,
agent,
agentForward: !!agent,
keepaliveInterval: connection.keepaliveInterval,
keepaliveCountMax: connection.keepaliveCountMax,
readyTimeout: connection.readyTimeout,
})
try {
ssh.connect({
host: session.connection.host,
port: session.connection.port || 22,
username: session.connection.user,
password: session.connection.privateKey ? undefined : '',
privateKey,
passphrase: privateKeyPassphrase,
tryKeyboard: true,
agent,
agentForward: !!agent,
keepaliveInterval: session.connection.keepaliveInterval,
keepaliveCountMax: session.connection.keepaliveCountMax,
readyTimeout: session.connection.readyTimeout,
hostVerifier: digest => {
log('SHA256 fingerprint: ' + digest)
return true
},
hostHash: 'sha256' as any,
algorithms: session.connection.algorithms,
})
} catch (e) {
this.toastr.error(e.message)
reject(e)
}
let keychainPasswordUsed = false
;(ssh as any).config.password = () => this.zone.run(async () => {
if (connection.password) {
this.logger.info('Using preset password')
return connection.password
if (session.connection.password) {
log('Using preset password')
return session.connection.password
}
if (!keychainPasswordUsed) {
let password = await this.passwordStorage.loadPassword(connection)
let password = await this.passwordStorage.loadPassword(session.connection)
if (password) {
this.logger.info('Using saved password')
log('Trying saved password')
keychainPasswordUsed = true
return password
}
}
let modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
modal.componentInstance.password = true
savedPassword = await modal.result
try {
savedPassword = await modal.result
} catch (_) {
return ''
}
return savedPassword
})
})
try {
let shell = await new Promise((resolve, reject) => {
let shell: any = await new Promise<any>((resolve, reject) => {
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
if (err) {
reject(err)
@@ -168,14 +211,17 @@ export class SSHService {
})
})
let session = new SSHSession(shell, connection)
session.shell = shell
return this.zone.run(() => this.app.openNewTab(
TerminalTabComponent,
{ session, sessionOptions: {} }
) as TerminalTabComponent)
shell.on('greeting', greeting => {
log('Shell Greeting: ' + greeting)
})
shell.on('banner', banner => {
log('Shell Banner: ' + banner)
})
} catch (error) {
console.log(error)
this.toastr.error(error.message)
throw error
}
}

View File

@@ -6,6 +6,7 @@ import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
@Injectable()
export class SSHSettingsTabProvider extends SettingsTabProvider {
id = 'ssh'
icon = 'globe'
title = 'SSH'
getComponentType (): any {

View File

@@ -12,7 +12,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-ssh:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},
@@ -44,6 +44,7 @@ module.exports = {
externals: [
'fs',
'node-ssh',
'ssh2-streams',
'xkeychain',
'wincredmgr',
'path',

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-terminal",
"version": "1.0.0-alpha.55",
"version": "1.0.68-c17-g8b64a81",
"description": "Terminus' terminal emulation core",
"keywords": [
"terminus-builtin-plugin"
@@ -17,7 +17,6 @@
"author": "Eugene Pankov",
"license": "MIT",
"devDependencies": {
"@terminus-term/xterm": "3.8.4",
"@types/deep-equal": "^1.0.0",
"@types/mz": "0.0.31",
"@types/node": "7.0.12",
@@ -27,6 +26,7 @@
"file-loader": "^0.11.2",
"rage-edit": "1.2.0",
"uuid": "^3.3.2",
"xterm": "3.10.1",
"xterm-addon-ligatures-tmp": "^0.1.0-beta-1"
},
"peerDependencies": {
@@ -40,10 +40,10 @@
"terminus-settings": "*"
},
"dependencies": {
"@terminus-term/node-pty": "0.8.0-1",
"font-manager": "0.3.0",
"hterm-umdjs": "1.4.1",
"mz": "^2.6.0",
"node-pty": "^0.8.0",
"ps-node": "^0.1.6",
"runes": "^0.4.2"
},

View File

@@ -1,10 +1,10 @@
import { TerminalTabComponent } from './components/terminalTab.component'
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
export abstract class TerminalDecorator {
// tslint:disable-next-line no-empty
attach (_terminal: TerminalTabComponent): void { }
attach (_terminal: BaseTerminalTabComponent): void { }
// tslint:disable-next-line no-empty
detach (_terminal: TerminalTabComponent): void { }
detach (_terminal: BaseTerminalTabComponent): void { }
}
export interface ResizeEvent {
@@ -44,7 +44,7 @@ export abstract class TerminalColorSchemeProvider {
export abstract class TerminalContextMenuItemProvider {
weight: number
abstract async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
abstract async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
}
export interface IShell {

View File

@@ -15,7 +15,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
hotkeys: HotkeysService,
) {
super()
if (!electron.remote.process.env.DEV) {
if (!electron.remote.process.env.TERMINUS_DEV) {
setImmediate(async () => {
let argv: string[] = electron.remote.process.argv
for (let arg of argv.slice(1).concat([electron.remote.process.argv0])) {

View File

@@ -1,6 +1,6 @@
h3.mb-3 Appearance
.row
.col-md-6
.d-flex
.mr-5
.form-line
.header
.title Frontend
@@ -26,6 +26,7 @@ h3.mb-3 Appearance
)
input.form-control.w-25(
type='number',
max='48',
[(ngModel)]='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
@@ -91,7 +92,7 @@ h3.mb-3 Appearance
[title]='idx',
)
.col-md-6
div
.form-group
.appearance-preview(
[style.font-family]='config.store.terminal.font',

View File

@@ -3,6 +3,9 @@
margin-left: 20px;
padding: 10px;
overflow: hidden;
max-width: 400px;
max-height: 400px;
span {
white-space: pre;
}

View File

@@ -5,7 +5,7 @@ import deepEqual = require('deep-equal')
const fontManager = require('font-manager')
import { Component, Inject } from '@angular/core'
import { ConfigService, HostAppService, Platform } from 'terminus-core'
import { ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
@Component({
@@ -22,6 +22,7 @@ export class AppearanceSettingsTabComponent {
constructor (
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
private hostApp: HostAppService,
private electron: ElectronService,
public config: ConfigService,
) { }
@@ -71,8 +72,16 @@ export class AppearanceSettingsTabComponent {
this.editingColorScheme = null
}
deleteScheme (scheme: ITerminalColorScheme) {
if (confirm(`Delete "${scheme.name}"?`)) {
async deleteScheme (scheme: ITerminalColorScheme) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `Delete "${scheme.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
let schemes = this.config.store.terminal.customColorSchemes
schemes = schemes.filter(x => x !== scheme)
this.config.store.terminal.customColorSchemes = schemes

View File

@@ -0,0 +1,374 @@
import { Observable, Subject, Subscription } from 'rxjs'
import { first } from 'rxjs/operators'
import { ToastrService } from 'ngx-toastr'
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input, ElementRef } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
import { BaseSession, SessionsService } from '../services/sessions.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service'
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
import { Frontend } from '../frontends/frontend'
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
static template = `
<div
#content
class="content"
[style.opacity]="htermVisible ? 1 : 0"
></div>
`
static styles = [require('./terminalTab.component.scss')]
session: BaseSession
@Input() zoom = 0
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
@HostBinding('class.top-padded') topPadded: boolean
frontend: Frontend
sessionCloseSubscription: Subscription
hotkeysSubscription: Subscription
htermVisible = false
frontendReady = new Subject<void>()
size: ResizeEvent
protected logger: Logger
protected output = new Subject<string>()
private bellPlayer: HTMLAudioElement
private termContainerSubscriptions: Subscription[] = []
get input$ (): Observable<string> { return this.frontend.input$ }
get output$ (): Observable<string> { return this.output }
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
get frontendReady$ (): Observable<void> { return this.frontendReady }
constructor (
public config: ConfigService,
public element: ElementRef,
protected injector: Injector,
protected zone: NgZone,
protected app: AppService,
protected hostApp: HostAppService,
protected hotkeys: HotkeysService,
protected sessions: SessionsService,
protected electron: ElectronService,
protected terminalContainersService: TerminalFrontendService,
protected toastr: ToastrService,
protected log: LogService,
@Optional() @Inject(TerminalDecorator) protected decorators: TerminalDecorator[],
@Optional() @Inject(TerminalContextMenuItemProvider) protected contextMenuProviders: TerminalContextMenuItemProvider[],
) {
super()
this.logger = log.create('baseTerminalTab')
this.decorators = this.decorators || []
this.setTitle('Terminal')
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) {
return
}
switch (hotkey) {
case 'ctrl-c':
if (this.frontend.getSelection()) {
this.frontend.copySelection()
this.frontend.clearSelection()
this.toastr.info('Copied')
} else {
this.sendInput('\x03')
}
break
case 'copy':
this.frontend.copySelection()
this.toastr.info('Copied')
break
case 'paste':
this.paste()
break
case 'clear':
this.frontend.clear()
break
case 'zoom-in':
this.zoomIn()
break
case 'zoom-out':
this.zoomOut()
break
case 'reset-zoom':
this.resetZoom()
break
case 'home':
this.sendInput('\x1bOH')
break
case 'end':
this.sendInput('\x1bOF')
break
case 'previous-word':
this.sendInput('\x1bb')
break
case 'next-word':
this.sendInput('\x1bf')
break
case 'delete-previous-word':
this.sendInput('\x1b\x7f')
break
case 'delete-next-word':
this.sendInput('\x1bd')
break
}
})
this.bellPlayer = document.createElement('audio')
this.bellPlayer.src = require<string>('../bell.ogg')
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
}
ngOnInit () {
this.focused$.subscribe(() => {
this.configure()
this.frontend.focus()
})
this.frontend = this.terminalContainersService.getFrontend(this.session)
this.frontend.ready$.subscribe(() => {
this.htermVisible = true
})
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
this.size = { columns, rows }
this.frontendReady.next()
setTimeout(() => {
this.session.resize(columns, rows)
}, 1000)
this.session.releaseInitialDataBuffer()
})
this.frontend.configure()
if (this.hasFocus) {
this.frontend.attach(this.content.nativeElement)
} else {
this.focused$.pipe(first()).subscribe(() => {
this.frontend.attach(this.content.nativeElement)
})
}
this.attachTermContainerHandlers()
this.configure()
this.config.enabledServices(this.decorators).forEach((decorator) => {
decorator.attach(this)
})
setTimeout(() => {
this.output.subscribe(() => {
this.displayActivity()
})
}, 1000)
this.frontend.bell$.subscribe(() => {
if (this.config.store.terminal.bell === 'visual') {
this.frontend.visualBell()
}
if (this.config.store.terminal.bell === 'audible') {
this.bellPlayer.play()
}
})
this.frontend.focus()
}
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = []
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
items = items.concat(section)
items.push({ type: 'separator' })
}
items.splice(items.length - 1, 1)
return items
}
detachTermContainerHandlers () {
for (let subscription of this.termContainerSubscriptions) {
subscription.unsubscribe()
}
this.termContainerSubscriptions = []
}
attachTermContainerHandlers () {
this.detachTermContainerHandlers()
this.termContainerSubscriptions = [
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
this.focused$.subscribe(() => this.frontend.enableResizing = true),
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
this.frontend.mouseEvent$.subscribe(async event => {
if (event.type === 'mousedown') {
if (event.which === 2) {
this.paste()
event.preventDefault()
event.stopPropagation()
return
}
if (event.which === 3) {
if (this.config.store.terminal.rightClick === 'menu') {
this.hostApp.popupContextMenu(await this.buildContextMenu())
} else if (this.config.store.terminal.rightClick === 'paste') {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
}
}
if (event.type === 'mousewheel') {
let wheelDeltaY = 0
if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
}
if (event.ctrlKey || event.metaKey) {
if (wheelDeltaY > 0) {
this.zoomIn()
} else {
this.zoomOut()
}
} else if (event.altKey) {
event.preventDefault()
let delta = Math.round(wheelDeltaY / 50)
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
}
}
}),
this.frontend.input$.subscribe(data => {
this.sendInput(data)
}),
this.frontend.resize$.subscribe(({ columns, rows }) => {
this.logger.debug(`Resizing to ${columns}x${rows}`)
this.size = { columns, rows }
this.zone.run(() => {
if (this.session && this.session.open) {
this.session.resize(columns, rows)
}
})
}),
this.hostApp.windowMoved$.subscribe(() => setTimeout(() => {
this.configure()
}, 250)),
this.hostApp.displayMetricsChanged$.subscribe(() => setTimeout(() => {
this.configure()
}, 250)),
]
}
sendInput (data: string) {
this.session.write(data)
if (this.config.store.terminal.scrollOnInput) {
this.frontend.scrollToBottom()
}
}
write (data: string) {
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
if (percentageMatch) {
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
if (percentage > 0 && percentage <= 100) {
this.setProgress(percentage)
this.logger.debug('Detected progress:', percentage)
}
} else {
this.setProgress(null)
}
this.frontend.write(data)
}
paste () {
let data = this.electron.clipboard.readText()
if (this.config.store.terminal.bracketedPaste) {
data = '\x1b[200~' + data + '\x1b[201~'
}
if (this.hostApp.platform === Platform.Windows) {
data = data.replace(/\r\n/g, '\r')
} else {
data = data.replace(/\n/g, '\r')
}
this.sendInput(data)
}
configure (): void {
this.frontend.configure()
this.topPadded = this.hostApp.platform === Platform.macOS
&& this.config.store.appearance.frame === 'thin'
&& this.config.store.appearance.tabsLocation === 'bottom'
if (this.config.store.terminal.background === 'colorScheme') {
if (this.config.store.terminal.colorScheme.background) {
this.backgroundColor = this.config.store.terminal.colorScheme.background
}
} else {
this.backgroundColor = null
}
}
zoomIn () {
this.zoom++
this.frontend.setZoom(this.zoom)
}
zoomOut () {
this.zoom--
this.frontend.setZoom(this.zoom)
}
resetZoom () {
this.zoom = 0
this.frontend.setZoom(this.zoom)
}
ngOnDestroy () {
this.frontend.detach(this.content.nativeElement)
this.detachTermContainerHandlers()
this.config.enabledServices(this.decorators).forEach(decorator => {
decorator.detach(this)
})
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
}
this.output.complete()
}
async destroy () {
super.destroy()
if (this.session && this.session.open) {
await this.session.destroy()
}
}
protected attachSessionHandlers () {
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => {
this.zone.run(() => {
this.output.next(data)
this.write(data)
})
})
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.frontend.destroy()
this.app.closeTab(this)
})
}
}

View File

@@ -25,6 +25,9 @@ h3.mb-3 Shell
(ngModelChange)='config.save()'
)
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.useConPTY && isConPTYAvailable && !isConPTYStable')
.mr-auto Windows 10 build 18309 or above is recommended for ConPTY
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.shell.startsWith("wsl") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
.mr-auto WSL terminal only supports TrueColor with ConPTY and the hterm frontend

View File

@@ -1,3 +1,4 @@
import * as os from 'os'
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
@@ -15,6 +16,7 @@ export class ShellSettingsTabComponent {
profiles: Profile[] = []
Platform = Platform
isConPTYAvailable: boolean
isConPTYStable: boolean
private configSubscription: Subscription
constructor (
@@ -31,6 +33,9 @@ export class ShellSettingsTabComponent {
})
this.reload()
this.isConPTYAvailable = uac.isAvailable
this.isConPTYStable = hostApp.platform === Platform.Windows
&& parseFloat(os.release()) >= 10
&& parseInt(os.release().split('.')[2]) >= 18309
}
async ngOnInit () {
@@ -48,10 +53,13 @@ export class ShellSettingsTabComponent {
pickWorkingDirectory () {
let shell = this.shells.find(x => x.id === this.config.store.terminal.shell)
console.log(shell)
let paths = this.electron.dialog.showOpenDialog({
defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],
})
let paths = this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
{
defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],
}
)
if (paths) {
this.config.store.terminal.workingDirectory = paths[0]
}

View File

@@ -3,6 +3,10 @@
display: flex;
overflow: hidden;
&.top-padded {
padding-top: 20px;
}
&> .content {
flex: auto;
position: relative;

View File

@@ -1,121 +1,27 @@
import { Observable, Subject, Subscription } from 'rxjs'
import { Component, Input } from '@angular/core'
import { first } from 'rxjs/operators'
import { ToastrService } from 'ngx-toastr'
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
import { Session, SessionsService } from '../services/sessions.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service'
import { TerminalDecorator, ResizeEvent, SessionOptions, TerminalContextMenuItemProvider } from '../api'
import { Frontend } from '../frontends/frontend'
import { BaseTabProcess } from 'terminus-core'
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
import { SessionOptions } from '../api'
import { Session } from '../services/sessions.service'
@Component({
selector: 'terminalTab',
template: `
<div
#content
class="content"
[style.opacity]="htermVisible ? 1 : 0"
></div>
`,
styles: [require('./terminalTab.component.scss')],
template: BaseTerminalTabComponent.template,
styles: BaseTerminalTabComponent.styles,
})
export class TerminalTabComponent extends BaseTabComponent {
session: Session
export class TerminalTabComponent extends BaseTerminalTabComponent {
@Input() sessionOptions: SessionOptions
@Input() zoom = 0
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
frontend: Frontend
sessionCloseSubscription: Subscription
hotkeysSubscription: Subscription
htermVisible = false
private output = new Subject<string>()
private bellPlayer: HTMLAudioElement
private termContainerSubscriptions: Subscription[] = []
get input$ (): Observable<string> { return this.frontend.input$ }
get output$ (): Observable<string> { return this.output }
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
constructor (
private zone: NgZone,
private app: AppService,
private hostApp: HostAppService,
private hotkeys: HotkeysService,
private sessions: SessionsService,
private electron: ElectronService,
private terminalContainersService: TerminalFrontendService,
public config: ConfigService,
private toastr: ToastrService,
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
@Optional() @Inject(TerminalContextMenuItemProvider) private contextMenuProviders: TerminalContextMenuItemProvider[],
) {
super()
this.decorators = this.decorators || []
this.setTitle('Terminal')
ngOnInit () {
this.logger = this.log.create('terminalTab')
this.session = new Session(this.config)
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) {
return
}
switch (hotkey) {
case 'ctrl-c':
if (this.frontend.getSelection()) {
this.frontend.copySelection()
this.frontend.clearSelection()
this.toastr.info('Copied')
} else {
this.sendInput('\x03')
}
break
case 'copy':
this.frontend.copySelection()
this.toastr.info('Copied')
break
case 'paste':
this.paste()
break
case 'clear':
this.frontend.clear()
break
case 'zoom-in':
this.zoomIn()
break
case 'zoom-out':
this.zoomOut()
break
case 'reset-zoom':
this.resetZoom()
break
case 'home':
this.sendInput('\x1bOH')
break
case 'end':
this.sendInput('\x1bOF')
break
case 'previous-word':
this.sendInput('\x1bb')
break
case 'next-word':
this.sendInput('\x1bf')
break
case 'delete-previous-word':
this.sendInput('\x1b\x7f')
break
case 'delete-next-word':
this.sendInput('\x1bd')
break
}
this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession(this.size.columns, this.size.rows)
})
this.bellPlayer = document.createElement('audio')
this.bellPlayer.src = require<string>('../bell.ogg')
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
super.ngOnInit()
}
initializeSession (columns: number, rows: number) {
@@ -127,18 +33,7 @@ export class TerminalTabComponent extends BaseTabComponent {
})
)
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => {
this.zone.run(() => {
this.output.next(data)
this.write(data)
})
})
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.frontend.destroy()
this.app.closeTab(this)
})
this.attachSessionHandlers()
}
async getRecoveryToken (): Promise<any> {
@@ -152,195 +47,6 @@ export class TerminalTabComponent extends BaseTabComponent {
}
}
ngOnInit () {
this.focused$.subscribe(() => {
this.configure()
this.frontend.focus()
})
this.frontend = this.terminalContainersService.getFrontend(this.session)
this.frontend.ready$.subscribe(() => {
this.htermVisible = true
})
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
if (!this.session.open) {
this.initializeSession(columns, rows)
}
setTimeout(() => {
this.session.resize(columns, rows)
}, 1000)
this.session.releaseInitialDataBuffer()
})
this.frontend.configure(this.config.store)
this.frontend.attach(this.content.nativeElement)
this.attachTermContainerHandlers()
this.configure()
this.config.enabledServices(this.decorators).forEach((decorator) => {
decorator.attach(this)
})
setTimeout(() => {
this.output.subscribe(() => {
this.displayActivity()
})
}, 1000)
this.frontend.bell$.subscribe(() => {
if (this.config.store.terminal.bell === 'visual') {
this.frontend.visualBell()
}
if (this.config.store.terminal.bell === 'audible') {
this.bellPlayer.play()
}
})
this.frontend.focus()
}
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = []
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
items = items.concat(section)
items.push({ type: 'separator' })
}
items.splice(items.length - 1, 1)
return items
}
detachTermContainerHandlers () {
for (let subscription of this.termContainerSubscriptions) {
subscription.unsubscribe()
}
this.termContainerSubscriptions = []
}
attachTermContainerHandlers () {
this.detachTermContainerHandlers()
this.termContainerSubscriptions = [
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
this.focused$.subscribe(() => this.frontend.enableResizing = true),
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
this.frontend.mouseEvent$.subscribe(async event => {
if (event.type === 'mousedown') {
if (event.which === 3) {
if (this.config.store.terminal.rightClick === 'menu') {
this.hostApp.popupContextMenu(await this.buildContextMenu())
} else if (this.config.store.terminal.rightClick === 'paste') {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
}
}
if (event.type === 'mousewheel') {
let wheelDeltaY = 0
if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
}
if (event.ctrlKey || event.metaKey) {
if (wheelDeltaY > 0) {
this.zoomIn()
} else {
this.zoomOut()
}
} else if (event.altKey) {
event.preventDefault()
let delta = Math.round(wheelDeltaY / 50)
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
}
}
}),
this.frontend.input$.subscribe(data => {
this.sendInput(data)
}),
this.frontend.resize$.subscribe(({ columns, rows }) => {
console.log(`Resizing to ${columns}x${rows}`)
this.zone.run(() => {
if (this.session.open) {
this.session.resize(columns, rows)
}
})
})
]
}
sendInput (data: string) {
this.session.write(data)
if (this.config.store.terminal.scrollOnInput) {
this.frontend.scrollToBottom()
}
}
write (data: string) {
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
if (percentageMatch) {
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
if (percentage > 0 && percentage <= 100) {
this.setProgress(percentage)
console.log('Detected progress:', percentage)
}
} else {
this.setProgress(null)
}
this.frontend.write(data)
}
paste () {
let data = this.electron.clipboard.readText()
if (this.config.store.terminal.bracketedPaste) {
data = '\x1b[200~' + data + '\x1b[201~'
}
if (this.hostApp.platform === Platform.Windows) {
data = data.replace(/\r\n/g, '\r')
} else {
data = data.replace(/\n/g, '\r')
}
this.sendInput(data)
}
configure (): void {
this.frontend.configure(this.config.store)
if (this.config.store.terminal.background === 'colorScheme') {
if (this.config.store.terminal.colorScheme.background) {
this.backgroundColor = this.config.store.terminal.colorScheme.background
}
} else {
this.backgroundColor = null
}
}
zoomIn () {
this.zoom++
this.frontend.setZoom(this.zoom)
}
zoomOut () {
this.zoom--
this.frontend.setZoom(this.zoom)
}
resetZoom () {
this.zoom = 0
this.frontend.setZoom(this.zoom)
}
async getCurrentProcess (): Promise<BaseTabProcess> {
let children = await this.session.getChildProcesses()
if (!children.length) {
@@ -351,47 +57,19 @@ export class TerminalTabComponent extends BaseTabComponent {
}
}
ngOnDestroy () {
this.frontend.detach(this.content.nativeElement)
this.detachTermContainerHandlers()
this.config.enabledServices(this.decorators).forEach(decorator => {
decorator.detach(this)
})
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
}
this.output.complete()
}
async destroy () {
super.destroy()
if (this.session && this.session.open) {
await this.session.destroy()
}
}
async canClose (): Promise<boolean> {
let children = await this.session.getChildProcesses()
if (children.length === 0) {
return true
}
return confirm(`"${children[0].command}" is still running. Close?`)
}
async saveAsProfile () {
let profile = {
sessionOptions: {
...this.sessionOptions,
cwd: (await this.session.getWorkingDirectory()) || this.sessionOptions.cwd,
},
name: this.sessionOptions.command,
}
this.config.store.terminal.profiles = [
...this.config.store.terminal.profiles,
profile,
]
this.config.save()
this.toastr.info('Saved')
return (await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `"${children[0].command}" is still running. Close?`,
buttons: ['Cancel', 'Kill'],
defaultId: 1,
}
)).response === 1
}
}

View File

@@ -8,7 +8,7 @@ export class TerminalConfigProvider extends ConfigProvider {
},
},
terminal: {
frontend: 'hterm',
frontend: 'xterm',
autoOpen: false,
fontSize: 14,
linePadding: 0,

View File

@@ -3,7 +3,7 @@ import { ToastrService } from 'ngx-toastr'
import { ConfigService } from 'terminus-core'
import { UACService } from './services/uac.service'
import { TerminalService } from './services/terminal.service'
import { TerminalTabComponent } from './components/terminalTab.component'
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
import { TerminalContextMenuItemProvider } from './api'
@Injectable()
@@ -19,14 +19,14 @@ export class NewTabContextMenu extends TerminalContextMenuItemProvider {
super()
}
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let shells = await this.terminalService.shells$.toPromise()
let items: Electron.MenuItemConstructorOptions[] = [
{
label: 'New terminal',
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(tab.sessionOptions)
this.terminalService.openTabWithOptions((tab as any).sessionOptions)
})
},
{
@@ -84,7 +84,7 @@ export class CopyPasteContextMenu extends TerminalContextMenuItemProvider {
super()
}
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Copy',

View File

@@ -1,7 +1,11 @@
import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
import { ResizeEvent } from '../api'
import { ConfigService, ThemesService } from 'terminus-core'
export abstract class Frontend {
configService: ConfigService
themesService: ThemesService
enableResizing = true
protected ready = new AsyncSubject<void>()
protected title = new ReplaySubject<string>(1)
@@ -54,6 +58,6 @@ export abstract class Frontend {
abstract visualBell (): void
abstract scrollToBottom (): void
abstract configure (configStore: any): void
abstract configure (): void
abstract setZoom (zoom: number): void
}

View File

@@ -51,7 +51,9 @@ export class HTermFrontend extends Frontend {
this.term.onVTKeystroke('\f')
}
configure (config: any): void {
configure (): void {
let config = this.configService.store
this.configuredFontSize = config.terminal.fontSize
this.configuredLinePadding = config.terminal.linePadding
this.setFontSize()
@@ -85,8 +87,7 @@ export class HTermFrontend extends Frontend {
preferenceManager.set('background-color', config.terminal.colorScheme.background)
}
} else {
// hterm can't parse "transparent"
preferenceManager.set('background-color', 'transparent')
preferenceManager.set('background-color', config.appearance.vibrancy ? 'transparent' : this.themesService.findCurrentTheme().terminalBackground)
}
this.configuredBackgroundColor = preferenceManager.get('background-color')

View File

@@ -1,8 +1,8 @@
import { Frontend } from './frontend'
import { Terminal, ITheme } from '@terminus-term/xterm'
import * as fit from '@terminus-term/xterm/src/addons/fit/fit'
import { Terminal, ITheme } from 'xterm'
import * as fit from 'xterm/src/addons/fit/fit'
import * as ligatures from 'xterm-addon-ligatures-tmp'
import '@terminus-term/xterm/lib/xterm.css'
import 'xterm/lib/xterm.css'
import './xterm.css'
import deepEqual = require('deep-equal')
@@ -12,9 +12,10 @@ Terminal.applyAddon(ligatures)
export class XTermFrontend extends Frontend {
enableResizing = true
xterm: Terminal
xtermCore: any
private configuredFontSize = 0
private zoom = 0
private resizeHandler: any
private resizeHandler: () => void
private configuredTheme: ITheme = {}
private copyOnSelect = false
@@ -24,6 +25,7 @@ export class XTermFrontend extends Frontend {
allowTransparency: true,
enableBold: true,
})
this.xtermCore = (this.xterm as any)._core
this.xterm.on('data', data => {
this.input.next(data)
@@ -39,6 +41,27 @@ export class XTermFrontend extends Frontend {
this.copySelection()
}
})
this.xterm.attachCustomKeyEventHandler((event: KeyboardEvent) => {
if ((event.getModifierState('Control') || event.getModifierState('Meta')) && event.key.toLowerCase() === 'v') {
event.preventDefault()
return false
}
if (event.getModifierState('Meta') && event.key.startsWith('Arrow')) {
return false
}
return true
})
this.xtermCore._scrollToBottom = this.xtermCore.scrollToBottom.bind(this.xtermCore)
this.xtermCore.scrollToBottom = () => null
this.resizeHandler = () => {
try {
(this.xterm as any).fit()
} catch {
// tends to throw when element wasn't shown yet
}
}
}
attach (host: HTMLElement): void {
@@ -46,7 +69,6 @@ export class XTermFrontend extends Frontend {
this.ready.next(null)
this.ready.complete()
this.resizeHandler = () => (this.xterm as any).fit()
window.addEventListener('resize', this.resizeHandler)
this.resizeHandler()
@@ -88,14 +110,21 @@ export class XTermFrontend extends Frontend {
}
visualBell (): void {
(this.xterm as any).bell()
this.xtermCore.bell()
}
scrollToBottom (): void {
this.xterm.scrollToBottom()
this.xtermCore._scrollToBottom()
}
configure (config: any): void {
configure (): void {
let config = this.configService.store
setTimeout(() => {
if (this.xterm.cols && this.xterm.rows) {
this.resizeHandler()
}
})
this.xterm.setOption('fontFamily', `"${config.terminal.font}", "monospace-fallback", monospace`)
this.xterm.setOption('bellStyle', config.terminal.bell)
this.xterm.setOption('cursorStyle', {
@@ -103,7 +132,7 @@ export class XTermFrontend extends Frontend {
}[config.terminal.cursor] || config.terminal.cursor)
this.xterm.setOption('cursorBlink', config.terminal.cursorBlink)
this.xterm.setOption('macOptionIsMeta', config.terminal.altIsMeta)
// this.xterm.setOption('colors', )
this.xterm.setOption('scrollback', 100000)
this.configuredFontSize = config.terminal.fontSize
this.setFontSize()
@@ -111,7 +140,7 @@ export class XTermFrontend extends Frontend {
let theme: ITheme = {
foreground: config.terminal.colorScheme.foreground,
background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : 'transparent',
background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : (config.appearance.vibrancy ? 'transparent' : this.themesService.findCurrentTheme().terminalBackground),
cursor: config.terminal.colorScheme.cursor,
}

View File

@@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core'
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService, TabContextMenuItemProvider } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component'
@@ -16,6 +16,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
import { ColorPickerComponent } from './components/colorPicker.component'
import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
import { BaseSession } from './services/sessions.service'
import { TerminalFrontendService } from './services/terminalFrontend.service'
@@ -31,6 +32,7 @@ import { TerminalConfigProvider } from './config'
import { TerminalHotkeyProvider } from './hotkeys'
import { HyperColorSchemes } from './colorSchemes'
import { NewTabContextMenu, CopyPasteContextMenu } from './contextMenu'
import { SaveAsProfileContextMenu } from './tabContextMenu'
import { CmderShellProvider } from './shells/cmder'
import { CustomShellProvider } from './shells/custom'
@@ -83,6 +85,8 @@ import { hterm } from './hterm'
{ provide: TerminalContextMenuItemProvider, useClass: NewTabContextMenu, multi: true },
{ provide: TerminalContextMenuItemProvider, useClass: CopyPasteContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true },
// For WindowsDefaultShellProvider
PowerShellCoreShellProvider,
WSLShellProvider,
@@ -203,5 +207,5 @@ export default class TerminalModule {
}
}
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService }
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService, BaseTerminalTabComponent }
export * from './api'

View File

@@ -5,14 +5,8 @@ import { TerminalTabComponent } from './components/terminalTab.component'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
constructor (
// private sessions: SessionsService,
) {
super()
}
async recover (recoveryToken: any): Promise<RecoveredTab> {
if (recoveryToken.type === 'app:terminal-tab') {
if (recoveryToken && recoveryToken.type === 'app:terminal-tab') {
return {
type: TerminalTabComponent,
options: { sessionOptions: recoveryToken.sessionOptions },

View File

@@ -104,16 +104,24 @@ export class Session extends BaseSession {
LC_MONETARY: locale,
})
}
let cwd = options.cwd || process.env.HOME
if (!fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = null
}
this.pty = nodePTY.spawn(options.command, options.args || [], {
name: 'xterm-256color',
cols: options.width || 80,
rows: options.height || 30,
cwd: options.cwd || process.env.HOME,
cwd,
env: env,
experimentalUseConpty: this.config.store.terminal.useConPTY,
experimentalUseConpty: this.config.store.terminal.useConPTY && 1,
})
this.guessedCWD = options.cwd || process.env.HOME
this.guessedCWD = cwd
this.truePID = (this.pty as any).pid
@@ -252,6 +260,14 @@ export class Session extends BaseSession {
return fs.readlink(`/proc/${this.truePID}/cwd`)
}
if (process.platform === 'win32') {
if (!this.guessedCWD) {
return null
}
try {
await fs.access(this.guessedCWD)
} catch (e) {
return null
}
return this.guessedCWD
}
return null
@@ -274,7 +290,7 @@ export class SessionsService {
constructor (
log: LogService,
) {
nodePTY = require('@terminus-term/node-pty')
nodePTY = require('node-pty')
nodePTY = require('../bufferizedPTY')(nodePTY)
this.logger = log.create('sessions')
}

View File

@@ -1,3 +1,4 @@
import * as fs from 'mz/fs'
import { Observable, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core'
import { AppService, Logger, LogService, ConfigService } from 'terminus-core'
@@ -41,6 +42,10 @@ export class TerminalService {
}
async openTab (shell?: IShell, cwd?: string, pause?: boolean): Promise<TerminalTabComponent> {
if (cwd && !fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = null
}
if (!cwd) {
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
cwd = await this.app.activeTab.session.getWorkingDirectory()

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { ConfigService } from 'terminus-core'
import { ConfigService, ThemesService } from 'terminus-core'
import { Frontend } from '../frontends/frontend'
import { HTermFrontend } from '../frontends/htermFrontend'
import { XTermFrontend } from '../frontends/xtermFrontend'
@@ -9,15 +9,21 @@ import { BaseSession } from '../services/sessions.service'
export class TerminalFrontendService {
private containers = new WeakMap<BaseSession, Frontend>()
constructor (private config: ConfigService) { }
constructor (private config: ConfigService, private themes: ThemesService) { }
getFrontend (session: BaseSession): Frontend {
getFrontend (session?: BaseSession): Frontend {
if (!session) {
let frontend: Frontend = (this.config.store.terminal.frontend === 'xterm')
? new XTermFrontend()
: new HTermFrontend()
frontend.configService = this.config
frontend.themesService = this.themes
return frontend
}
if (!this.containers.has(session)) {
this.containers.set(
session,
(this.config.store.terminal.frontend === 'xterm')
? new XTermFrontend()
: new HTermFrontend()
this.getFrontend(),
)
}
return this.containers.get(session)

View File

@@ -25,7 +25,7 @@ export class UACService {
'UAC.exe',
)
if (process.env.DEV) {
if (process.env.TERMINUS_DEV) {
helperPath = path.join(
path.dirname(this.electron.app.getPath('exe')),
'..', '..', '..',

View File

@@ -8,6 +8,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
@Injectable()
export class AppearanceSettingsTabProvider extends SettingsTabProvider {
id = 'terminal-appearance'
icon = 'palette'
title = 'Appearance'
getComponentType (): any {
@@ -18,6 +19,7 @@ export class AppearanceSettingsTabProvider extends SettingsTabProvider {
@Injectable()
export class ShellSettingsTabProvider extends SettingsTabProvider {
id = 'terminal-shell'
icon = 'list-ul'
title = 'Shell'
getComponentType (): any {
@@ -28,6 +30,7 @@ export class ShellSettingsTabProvider extends SettingsTabProvider {
@Injectable()
export class TerminalSettingsTabProvider extends SettingsTabProvider {
id = 'terminal'
icon = 'terminal'
title = 'Terminal'
getComponentType (): any {

View File

@@ -0,0 +1,41 @@
import { Injectable, NgZone } from '@angular/core'
import { ToastrService } from 'ngx-toastr'
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider } from 'terminus-core'
import { TerminalTabComponent } from './components/terminalTab.component'
@Injectable()
export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
constructor (
private config: ConfigService,
private zone: NgZone,
private toastr: ToastrService,
) {
super()
}
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
if (!(tab instanceof TerminalTabComponent)) {
return []
}
return [
{
label: 'Save as profile',
click: () => this.zone.run(async () => {
let profile = {
sessionOptions: {
...tab.sessionOptions,
cwd: (await tab.session.getWorkingDirectory()) || tab.sessionOptions.cwd,
},
name: tab.sessionOptions.command,
}
this.config.store.terminal.profiles = [
...this.config.store.terminal.profiles,
profile,
]
this.config.save()
this.toastr.info('Saved')
})
}
]
}
}

View File

@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-terminal:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization: {
minimize: false,
},
@@ -61,7 +61,7 @@ module.exports = {
'windows-process-tree',
'mz/fs',
'mz/child_process',
'@terminus-term/node-pty',
'node-pty',
/^rxjs/,
/^@angular/,
/^@ng-bootstrap/,

View File

@@ -2,18 +2,6 @@
# yarn lockfile v1
"@terminus-term/node-pty@0.8.0-1":
version "0.8.0-1"
resolved "https://registry.yarnpkg.com/@terminus-term/node-pty/-/node-pty-0.8.0-1.tgz#3cb682f2351179842d195c074acf7d5b54e96ffe"
integrity sha512-mEP5zQC/yHtvCbYjdGmwzFkkdTqCe0Jfd1o35yzM9jfGrVpW9qlvo/ZrzyOLSH2tJlYRB5SqfdWlo/LVXrAEYA==
dependencies:
nan "2.10.0"
"@terminus-term/xterm@3.8.4":
version "3.8.4"
resolved "https://registry.yarnpkg.com/@terminus-term/xterm/-/xterm-3.8.4.tgz#c9a9d9e0d46dbd8a94e06384e2d7268d36f5b0c6"
integrity sha512-DrxCjnJh9n3ivpldwI098PnuVYwg9e5lFlU8/1qfh/J/wFHbG3dX/bEtB4ynfTi3IXVJozFO2psD96+W2h3yeQ==
"@types/deep-equal@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
@@ -27,9 +15,9 @@
"@types/node" "*"
"@types/node@*":
version "10.12.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
integrity sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==
version "10.12.19"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.19.tgz#ca1018c26be01f07e66ac7fefbeb6407e4490c61"
integrity sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==
"@types/node@7.0.12":
version "7.0.12"
@@ -46,10 +34,10 @@ any-promise@^1.0.0:
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
connected-domain@^1.0.0:
version "1.0.0"
@@ -112,24 +100,26 @@ hterm-umdjs@1.4.1:
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957"
integrity sha512-r5JOmdDK1bZCmp3cKcuGRLVeum33H+pzD119ZxmQou+QUVe6SAVSz03HvKWVhM2Ao1Biv+fkhFDmnsaRPq0tFg==
json5@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
dependencies:
minimist "^1.2.0"
loader-utils@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
version "1.2.3"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
dependencies:
big.js "^3.1.3"
big.js "^5.2.2"
emojis-list "^2.0.0"
json5 "^0.5.0"
json5 "^1.0.1"
lru-cache@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
@@ -141,6 +131,11 @@ macos-native-processlist@^1.0.0:
dependencies:
nan "^2.10.0"
minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
mz@^2.6.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
@@ -150,15 +145,17 @@ mz@^2.6.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==
nan@2.12.1, nan@>=2.10.0, nan@^2.10.0:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
nan@>=2.10.0, nan@^2.10.0:
version "2.11.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
node-pty@^0.8.0:
version "0.8.1"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.1.tgz#94b457bec013e7a09b8d9141f63b0787fa25c23f"
integrity sha512-j+/g0Q5dR+vkELclpJpz32HcS3O/3EdPSGPvDXJZVJQLCvgG0toEbfmymxAEyQyZEpaoKHAcoL+PvKM+4N9nlw==
dependencies:
nan "2.12.1"
object-assign@^4.0.1:
version "4.1.1"
@@ -245,6 +242,11 @@ xterm-addon-ligatures-tmp@^0.1.0-beta-1:
font-finder "^1.0.2"
font-ligatures "^1.3.1"
xterm@3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.10.1.tgz#14accf92772e5a6728f317a3c209ba714b73c8b5"
integrity sha512-RHaUwJ8zwLiICu1QsXoxUHP+R2Pp8Rc8yVoNali/nKw3CVXwmXxT/4mgbk7U22psuNgOqLyI4Sg9nlQfYeTRQw==
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"

123
yarn.lock
View File

@@ -407,25 +407,30 @@ app-builder-bin@2.6.0:
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-2.6.0.tgz#b4e5d5ee5bcf264818ab9830b95338f9f419de5d"
integrity sha512-7HphDMS2U9MwAA6R7lSU6MASFR/D+VJDb5hQ4Fn2coOMyaRn71QDWPdG0TPnDr88F2I7bsTuHYud28S/yN2lZw==
app-builder-lib@20.38.2, app-builder-lib@~20.38.2:
version "20.38.2"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-20.38.2.tgz#7b5b239ba7ce52cd618a91f5e499068e54a35a95"
integrity sha512-jwysFwaU4ohvHqv5jNCeoZSO0N/8x7W/c0S6TiTb6QUC3U0YVcsN7DPMj7QApHzTvMTO9kxzjUzwA8dbQZVovg==
app-builder-bin@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-2.6.1.tgz#aa97f82d341dfa6f1269d78955482d619cc613ed"
integrity sha512-W0l85O+s6lOaziWqAhszPfwiG0s15FvMBP9j9i/bknsMccUkwN60u4Cy7yYtf6akCUDuJenLqpTX4/xvkq1egw==
app-builder-lib@20.38.4, app-builder-lib@~20.38.3:
version "20.38.4"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-20.38.4.tgz#71a515d01f4f2bd48a67495804f659a46c35303c"
integrity sha512-JbuAJQBndcCW6BJVIb2tPjM5wiuIjz2LUlbyVxNIawM2wFKUBV9kr0N3RNBJFxcrCEuA9oprMUCoymJdrMUVfA==
dependencies:
"7zip-bin" "~4.1.0"
app-builder-bin "2.6.0"
app-builder-bin "2.6.1"
async-exit-hook "^2.0.1"
bluebird-lst "^1.0.6"
builder-util "9.6.0"
builder-util-runtime "8.0.2"
builder-util "9.6.1"
builder-util-runtime "8.1.0"
chromium-pickle-js "^0.2.0"
debug "^4.1.0"
ejs "^2.6.1"
electron-osx-sign "0.4.11"
electron-publish "20.38.2"
electron-publish "20.38.3"
fs-extra-p "^7.0.0"
hosted-git-info "^2.7.1"
is-ci "^1.2.1"
is-ci "^2.0.0"
isbinaryfile "^3.0.3"
js-yaml "^3.12.0"
lazy-val "^1.0.3"
@@ -1010,7 +1015,17 @@ builder-util-runtime@4.4.1, builder-util-runtime@^4.4.1:
fs-extra-p "^4.6.1"
sax "^1.2.4"
builder-util-runtime@8.0.2, builder-util-runtime@^8.0.2:
builder-util-runtime@8.1.0, builder-util-runtime@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.1.0.tgz#dd7fca995d48ceee7580b4851ca057566c94601e"
integrity sha512-s1mlJ28mv+56Iebh6c9aXjVe11O3Z0cDTwAGeB0PCcUzHA37fDxGgS8ZGoYNMZP+rBHj21d/od1wuYofTVLaQg==
dependencies:
bluebird-lst "^1.0.6"
debug "^4.1.0"
fs-extra-p "^7.0.0"
sax "^1.2.4"
builder-util-runtime@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.0.2.tgz#9d638a266ca3aa25ced1cff4df74c8fd97dd78cf"
integrity sha512-46AjyMQ1/yBvGnXWmqNGlg8te7jCPCs7TJ0zDC2+4vV/t5iZp2dR1H9UfVpcBxlvBq3dlAOmwb9fz1d9xZN1+Q==
@@ -1040,7 +1055,25 @@ builder-util@6.1.3, builder-util@~6.1.3:
stat-mode "^0.2.2"
temp-file "^3.1.3"
builder-util@9.6.0, builder-util@~9.6.0:
builder-util@9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-9.6.1.tgz#4625620b1535fe40dcacb178d24fe56d0d7c8957"
integrity sha512-8MljKTjeV+A+LLVexuWEV3EpWbiUcsHHrB4Bg2qNo/3dC+vTo6g/27+W3Ij7Ij1UTobSkNBstFieWijXJCco9A==
dependencies:
"7zip-bin" "~4.1.0"
app-builder-bin "2.6.1"
bluebird-lst "^1.0.6"
builder-util-runtime "^8.1.0"
chalk "^2.4.1"
debug "^4.1.0"
fs-extra-p "^7.0.0"
is-ci "^2.0.0"
js-yaml "^3.12.0"
source-map-support "^0.5.9"
stat-mode "^0.2.2"
temp-file "^3.3.2"
builder-util@~9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-9.6.0.tgz#ffcc0f713d0f4dfa6bcda2aee83b8fcb1f16f5b6"
integrity sha512-6T4E3aNVndTZ2oCt+22S0wxt47d094MxrADi6S012QumXlDNfSsyu1ffbGN9w0HG+4aubpLzf9apKgMP1yl4Kw==
@@ -1320,6 +1353,11 @@ ci-info@^1.5.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@@ -2055,12 +2093,12 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dmg-builder@6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-6.5.2.tgz#cb6e34b793831349f46c7e6415182d90e8c4a386"
integrity sha512-eT3qc8IrwfDyq5ddGO807Wya2ltVlIlE0FVf6Aa+HWdlp9JnYayPNGWrQA9xIHpKKlq206JdNZ6LYIn93EAzdg==
dmg-builder@6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-6.5.3.tgz#95afe3deab33fd874f68d299bc71b481e94f5312"
integrity sha512-ZNl4GFBg6rdFplnuoK56iftxh/qgM7rXJUxgl21eK4WsjxgQwtQ0REZo+pDSL4OzVeyOO8MMNWSNQcCsBLiDyA==
dependencies:
app-builder-lib "~20.38.2"
app-builder-lib "~20.38.3"
bluebird-lst "^1.0.6"
builder-util "~9.6.0"
fs-extra-p "^7.0.0"
@@ -2160,19 +2198,19 @@ electron-builder-squirrel-windows@^20.28.3:
optionalDependencies:
"7zip-bin" "~4.0.2"
electron-builder@^20.38.2:
version "20.38.2"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.2.tgz#e44f086348f70e40bad796b260a7d3542cb93282"
integrity sha512-uUEzfc/e8J7nAowvFQw4SyHIe4d6VSHO1LmcLy53he4aGXlVklHluhbwa0rxATPdYVNgHmJz7zoVgYYOd/YS+A==
electron-builder@^20.38.4:
version "20.38.4"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.4.tgz#67727529ffb87e7fdd78b3a84ea0d6c22bf04ec2"
integrity sha512-WHOr3Rz2wktxV5TqmRL6woO9/wrIZeRfJPSEXOhgfgLskE5Sp2Aer0zAF7lHNqXuG6JhU+0I9IYFAxa73MTs9w==
dependencies:
app-builder-lib "20.38.2"
app-builder-lib "20.38.4"
bluebird-lst "^1.0.6"
builder-util "9.6.0"
builder-util-runtime "8.0.2"
builder-util "9.6.1"
builder-util-runtime "8.1.0"
chalk "^2.4.1"
dmg-builder "6.5.2"
dmg-builder "6.5.3"
fs-extra-p "^7.0.0"
is-ci "^1.2.1"
is-ci "^2.0.0"
lazy-val "^1.0.3"
read-config-file "3.2.0"
sanitize-filename "^1.6.1"
@@ -2252,14 +2290,14 @@ electron-publish@20.28.3:
lazy-val "^1.0.3"
mime "^2.3.1"
electron-publish@20.38.2:
version "20.38.2"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-20.38.2.tgz#b0a59ee4435c509caa633f9fed01913addd48e1c"
integrity sha512-GXwnZm9I9l4RjlDwuALpR57aIH38qRzDEYbhLysmrC5T2xlCgyBIfxS5EUNESqKT+9KeJJZcj+eTKMjbz+Qafw==
electron-publish@20.38.3:
version "20.38.3"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-20.38.3.tgz#7c162904f728ba2bbf2640bc3620b65ce1061ce3"
integrity sha512-Qomq253NT5DfjUZgFSx6p+gheU5YhM6zZ67fTtBZvwyk0v8HwxNXfa8fZT7h+1c3BwEmjusTbmjZRNW/XZBXFA==
dependencies:
bluebird-lst "^1.0.6"
builder-util "~9.6.0"
builder-util-runtime "^8.0.2"
builder-util-runtime "^8.1.0"
chalk "^2.4.1"
fs-extra-p "^7.0.0"
lazy-val "^1.0.3"
@@ -2286,10 +2324,10 @@ electron-to-chromium@^1.2.7:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.58.tgz#8267a4000014e93986d9d18c65a8b4022ca75188"
integrity sha512-AGJxlBEn2wOohxqWZkISVsOjZueKTQljfEODTDSEiMqSpH0S+xzV+/5oEM9AGaqhu7DzrpKOgU7ocQRjj0nJmg==
electron@4.0.0-beta.7:
version "4.0.0-beta.7"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.0-beta.7.tgz#d54f3fb19910da593408ba6b4320d2bdf5c039da"
integrity sha512-770Hzxq10nQrzq39uVmvHLNKpPY3TCNrk+IYGDQTNWqmkreXZX6+9iflMMo+xdg1ZHysrTj1QQZvsjjBY176pg==
electron@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.0.tgz#6ccb40cc8bf2d49954dcea73b97ae7ad12ee04b3"
integrity sha512-3XPG/3IXlvnT1oe1K6zEushoD0SKbP8xwdrL10EWGe6k2iOV4hSHqJ8vWnR8yZ7VbSXmBRfomEFDNAo/q/cwKw==
dependencies:
"@types/node" "^8.0.24"
electron-download "^4.1.0"
@@ -3428,6 +3466,13 @@ is-ci@^1.0.10, is-ci@^1.2.0, is-ci@^1.2.1:
dependencies:
ci-info "^1.5.0"
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -4528,10 +4573,10 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-abi@^2.0.0, node-abi@^2.4.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.5.0.tgz#942e1a78bce764bc0c1672d5821e492b9d032052"
integrity sha512-9g2twBGSP6wIR5PW7tXvAWnEWKJDH/VskdXp168xsw9VVxpEGov8K4jsP4/VeoC7b2ZAyzckvMCuQuQlw44lXg==
node-abi@^2.0.0, node-abi@^2.4.4, node-abi@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.5.1.tgz#bb17288fc3b2f68fea0ed9897c66979fd754ed47"
integrity sha512-oDbFc7vCFx0RWWCweTer3hFm1u+e60N5FtGnmRV6QqvgATGFH/XRR6vqWIeBVosCYCqt6YdIr2L0exLZuEdVcQ==
dependencies:
semver "^5.4.1"
@@ -4544,7 +4589,7 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-gyp@^3.6.0, node-gyp@^3.6.2, node-gyp@^3.8.0:
node-gyp@^3.6.0, node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==