Compare commits
48 Commits
v1.0.0-alp
...
v1.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c3c983daf6 | ||
![]() |
dce8647f55 | ||
![]() |
f947fe3f0f | ||
![]() |
b5f96a59f8 | ||
![]() |
c90a5678cf | ||
![]() |
663da34e6d | ||
![]() |
049f08b8f9 | ||
![]() |
3c3b14bf09 | ||
![]() |
5e07dd5442 | ||
![]() |
8f2d2cbe30 | ||
![]() |
bebde4799d | ||
![]() |
9cedeb3efb | ||
![]() |
63158ac6cd | ||
![]() |
4f44087989 | ||
![]() |
ab3c49b9b2 | ||
![]() |
28d01a1b56 | ||
![]() |
a979f0108e | ||
![]() |
3c74b8ec38 | ||
![]() |
9d7bf2ae44 | ||
![]() |
3b43b3914b | ||
![]() |
e9f22dd8b5 | ||
![]() |
e68cafdb70 | ||
![]() |
fde16b8699 | ||
![]() |
245c65d750 | ||
![]() |
c7d9f944d5 | ||
![]() |
4ca806e142 | ||
![]() |
0255985bc6 | ||
![]() |
104f1ee7aa | ||
![]() |
132d0553ae | ||
![]() |
b007ff6ff6 | ||
![]() |
2bea4b9d6c | ||
![]() |
4a76c12f15 | ||
![]() |
181f3e3d33 | ||
![]() |
ee2fadbf60 | ||
![]() |
4259d3b53d | ||
![]() |
65aaa131ef | ||
![]() |
46d9aabbdd | ||
![]() |
692045ce77 | ||
![]() |
9c257b0002 | ||
![]() |
15c23eb7dd | ||
![]() |
5fc67d3648 | ||
![]() |
571884f39c | ||
![]() |
ccbcd30813 | ||
![]() |
30666c2838 | ||
![]() |
953558a866 | ||
![]() |
ace81aced2 | ||
![]() |
dc781deeb0 | ||
![]() |
e3d1d5e61e |
@@ -14,11 +14,11 @@
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/16x16.png"
|
||||
inkscape:export-xdpi="2.7093334"
|
||||
inkscape:export-ydpi="2.7093334">
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png"
|
||||
inkscape:export-xdpi="86.699997"
|
||||
inkscape:export-ydpi="86.699997">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
@@ -29,8 +29,8 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="134.39743"
|
||||
inkscape:cy="340.43068"
|
||||
inkscape:cx="85.897128"
|
||||
inkscape:cy="375.72042"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
@@ -43,7 +43,9 @@
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-paths="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
@@ -64,24 +66,24 @@
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.305965,108.47713 60.922105,35.13225 0.0945,21.68327 -61.016595,-37.11662 z"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 33.048081,103.66303 101.30357,143.02426 80.80219,154.86063 33.048089,125.73315 Z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666cc;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 136.19445,144.4429 0.0455,20.67266 -78.028381,44.11611 -0.0031,-19.78119 z"
|
||||
style="opacity:0.9;fill:#6666af;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 141.59934,143.95811 0.051,23.16109 -87.420905,49.42651 -0.0034,-22.16232 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.471179,178.6501 18.737341,10.818 0.0031,19.78099 -18.740409,-10.88245 z"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 33.233182,182.28294 20.992812,12.1202 0.0034,22.16208 -20.996251,-12.19239 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#b4e2ff;fill-rule:evenodd;stroke:none;stroke-width:1.00546169px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 56.43263,98.242186 -17.391087,10.041014 61.186527,35.32618 -61.020778,35.23005 18.839694,10.87703 61.020784,-35.23005 17.39108,-10.04102 z"
|
||||
style="opacity:0.9;fill:#9dbef0;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12649226px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 52.236336,92.196079 -19.484508,11.249681 68.551742,39.5785 -68.366041,39.4708 21.107487,12.18633 68.366044,-39.4708 19.48451,-11.24968 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
app/assets/tray-darwinHighlightTemplate.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
app/assets/tray-darwinHighlightTemplate@2x.png
Normal file
After Width: | Height: | Size: 955 B |
BIN
app/assets/tray-darwinTemplate.png
Normal file
After Width: | Height: | Size: 365 B |
BIN
app/assets/tray-darwinTemplate@2x.png
Normal file
After Width: | Height: | Size: 894 B |
BIN
app/assets/tray.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
50
app/bufferizedPTY.js
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = function patchPTYModule (path) {
|
||||
const mod = require(path)
|
||||
const oldSpawn = mod.spawn
|
||||
if (mod.patched) {
|
||||
return mod
|
||||
}
|
||||
mod.patched = true
|
||||
mod.spawn = (file, args, opt) => {
|
||||
let terminal = oldSpawn(file, args, opt)
|
||||
let timeout = null
|
||||
let buffer = ''
|
||||
let lastFlush = 0
|
||||
let nextTimeout = 0
|
||||
|
||||
const maxWindow = 250
|
||||
const minWindow = 50
|
||||
|
||||
function flush () {
|
||||
if (buffer) {
|
||||
terminal.emit('data-buffered', buffer)
|
||||
}
|
||||
lastFlush = Date.now()
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
function reschedule () {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
nextTimeout = Date.now() + minWindow
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
flush()
|
||||
}, minWindow)
|
||||
}
|
||||
|
||||
terminal.on('data', data => {
|
||||
buffer += data
|
||||
if (Date.now() - lastFlush > maxWindow) {
|
||||
flush()
|
||||
} else {
|
||||
if (Date.now() > nextTimeout - (minWindow / 10)) {
|
||||
reschedule()
|
||||
}
|
||||
}
|
||||
})
|
||||
return terminal
|
||||
}
|
||||
return mod
|
||||
}
|
68
app/main.js
@@ -1,11 +1,9 @@
|
||||
if (process.platform == 'win32' && require('electron-squirrel-startup')) process.exit(0)
|
||||
|
||||
const electron = require('electron')
|
||||
if (process.argv.indexOf('--debug') !== -1) {
|
||||
require('electron-debug')({enabled: true, showDevTools: 'undocked'})
|
||||
}
|
||||
|
||||
|
||||
let app = electron.app
|
||||
|
||||
let secondInstance = app.makeSingleInstance((argv, cwd) => {
|
||||
@@ -32,8 +30,21 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
setupWindowManagement = () => {
|
||||
app.window.on('show', () => {
|
||||
app.window.webContents.send('host:window-shown')
|
||||
if (app.tray) {
|
||||
app.tray.destroy()
|
||||
app.tray = null
|
||||
}
|
||||
})
|
||||
|
||||
app.window.on('hide', (e) => {
|
||||
if (!app.tray) {
|
||||
setupTray()
|
||||
}
|
||||
})
|
||||
|
||||
app.window.on('enter-full-screen', () => app.window.webContents.send('host:window-enter-full-screen'))
|
||||
app.window.on('leave-full-screen', () => app.window.webContents.send('host:window-leave-full-screen'))
|
||||
|
||||
app.window.on('close', (e) => {
|
||||
windowConfig.set('windowBoundaries', app.window.getBounds())
|
||||
})
|
||||
@@ -46,14 +57,6 @@ setupWindowManagement = () => {
|
||||
app.window.focus()
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-toggle-focus', () => {
|
||||
if (app.window.isFocused()) {
|
||||
app.window.minimize()
|
||||
} else {
|
||||
app.window.focus()
|
||||
}
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-maximize', () => {
|
||||
app.window.maximize()
|
||||
})
|
||||
@@ -75,20 +78,7 @@ setupWindowManagement = () => {
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-set-bounds', (event, bounds) => {
|
||||
let actualBounds = app.window.getBounds()
|
||||
actualBounds.width -= bounds.x - actualBounds.x
|
||||
actualBounds.height -= bounds.y - actualBounds.y
|
||||
actualBounds.x = bounds.x
|
||||
actualBounds.y = bounds.y
|
||||
app.window.setBounds(actualBounds)
|
||||
setTimeout(() => {
|
||||
actualBounds = app.window.getBounds()
|
||||
bounds.width += bounds.x - actualBounds.x
|
||||
bounds.height += bounds.y - actualBounds.y
|
||||
bounds.x = actualBounds.x
|
||||
bounds.y = actualBounds.y
|
||||
app.window.setBounds(bounds)
|
||||
}, 100)
|
||||
app.window.setBounds(bounds)
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
|
||||
@@ -97,6 +87,35 @@ setupWindowManagement = () => {
|
||||
}
|
||||
|
||||
|
||||
setupTray = () => {
|
||||
if (process.platform == 'darwin') {
|
||||
app.tray = new electron.Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`)
|
||||
app.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`)
|
||||
} else {
|
||||
app.tray = new electron.Tray(`${app.getAppPath()}/assets/tray.png`)
|
||||
}
|
||||
|
||||
app.tray.on('click', () => {
|
||||
app.window.show()
|
||||
app.window.focus()
|
||||
})
|
||||
|
||||
const contextMenu = electron.Menu.buildFromTemplate([{
|
||||
label: 'Show',
|
||||
click () {
|
||||
app.window.show()
|
||||
app.window.focus()
|
||||
}
|
||||
}])
|
||||
|
||||
if (process.platform != 'darwin') {
|
||||
app.tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
app.tray.setToolTip(`Terminus ${app.getVersion()}`)
|
||||
}
|
||||
|
||||
|
||||
setupMenu = () => {
|
||||
let template = [{
|
||||
label: "Application",
|
||||
@@ -157,7 +176,6 @@ setupMenu = () => {
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{role: 'close'},
|
||||
{role: 'minimize'},
|
||||
{role: 'zoom'},
|
||||
{type: 'separator'},
|
||||
|
@@ -27,6 +27,7 @@
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"js-yaml": "3.8.2",
|
||||
"mz": "^2.6.0",
|
||||
"ngx-toastr": "^8.0.0",
|
||||
"path": "0.12.7",
|
||||
"rxjs": "5.3.0",
|
||||
"zone.js": "0.8.12"
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
export function getRootModule (plugins: any[]) {
|
||||
let imports = [
|
||||
BrowserModule,
|
||||
...plugins,
|
||||
NgbModule.forRoot(),
|
||||
ToastrModule.forRoot({
|
||||
positionClass: 'toast-bottom-center',
|
||||
preventDuplicates: true,
|
||||
extendedTimeOut: 5000,
|
||||
}),
|
||||
]
|
||||
let bootstrap = [
|
||||
...(plugins.filter(x => x.bootstrap).map(x => x.bootstrap)),
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'source-sans-pro'
|
||||
import 'font-awesome/css/font-awesome.css'
|
||||
import 'ngx-toastr/toastr.css'
|
||||
import './preload.scss'
|
||||
|
||||
import * as Raven from 'raven-js'
|
||||
|
@@ -2,6 +2,7 @@ import 'zone.js'
|
||||
import 'core-js/es7/reflect'
|
||||
import 'core-js/core/delay'
|
||||
import 'rxjs'
|
||||
import './toastr.scss'
|
||||
|
||||
// Always land on the start view
|
||||
location.hash = ''
|
||||
|
@@ -59,6 +59,7 @@ const builtinModules = [
|
||||
'@angular/platform-browser',
|
||||
'@angular/platform-browser-dynamic',
|
||||
'@ng-bootstrap/ng-bootstrap',
|
||||
'ngx-toastr',
|
||||
'rxjs',
|
||||
'terminus-core',
|
||||
'terminus-settings',
|
||||
|
@@ -39,7 +39,7 @@
|
||||
.terminus-logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
background: url('./logo.svg');
|
||||
background: url('../assets/logo.svg');
|
||||
background-repeat: none;
|
||||
background-size: contain;
|
||||
margin: auto;
|
||||
|
16
app/src/toastr.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
#toast-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.toast {
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,.25);
|
||||
padding: 10px;
|
||||
background-image: none;
|
||||
width: auto;
|
||||
|
||||
&.toast-info {
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
||||
}
|
@@ -58,6 +58,7 @@ module.exports = {
|
||||
'child_process': 'commonjs child_process',
|
||||
'electron': 'commonjs electron',
|
||||
'electron-is-dev': 'commonjs electron-is-dev',
|
||||
'ngx-toastr': 'commonjs ngx-toastr',
|
||||
'module': 'commonjs module',
|
||||
'mz': 'commonjs mz',
|
||||
'path': 'commonjs path',
|
||||
|
@@ -195,6 +195,10 @@ mz@^2.6.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
ngx-toastr@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-8.0.0.tgz#f3bc53146b2f7da3eabf3daa1b1bbdf65cb49697"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 644 B |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -15,19 +15,22 @@
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
sodipodi:docname="icon.svg">
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png"
|
||||
inkscape:export-xdpi="86.699997"
|
||||
inkscape:export-ydpi="86.699997">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4649">
|
||||
<stop
|
||||
style="stop-color:#000916;stop-opacity:1"
|
||||
style="stop-color:#000316;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4645" />
|
||||
<stop
|
||||
style="stop-color:#004565;stop-opacity:1"
|
||||
style="stop-color:#190065;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4647" />
|
||||
</linearGradient>
|
||||
@@ -39,7 +42,8 @@
|
||||
y1="85.146751"
|
||||
x2="89.26284"
|
||||
y2="229.47229"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.82182032,0,0,0.82182032,15.208802,28.029361)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
@@ -49,8 +53,8 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="134.39743"
|
||||
inkscape:cy="340.43068"
|
||||
inkscape:cx="85.897128"
|
||||
inkscape:cy="375.72042"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
@@ -63,7 +67,9 @@
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-paths="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
@@ -83,34 +89,34 @@
|
||||
transform="translate(-10.356544,-82.309525)">
|
||||
<rect
|
||||
id="rect168"
|
||||
width="150"
|
||||
height="150"
|
||||
x="10.356544"
|
||||
y="82.309525"
|
||||
style="fill:url(#linearGradient4651);fill-opacity:1;stroke-width:0.26458332"
|
||||
rx="10"
|
||||
ry="10" />
|
||||
width="123.27305"
|
||||
height="123.27305"
|
||||
x="23.72002"
|
||||
y="95.673004"
|
||||
style="fill:url(#linearGradient4651);fill-opacity:1;stroke-width:0.21743995"
|
||||
rx="8.2182035"
|
||||
ry="8.2182035" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.305965,108.47713 60.922105,35.13225 0.0945,21.68327 -61.016595,-37.11662 z"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 47.511243,117.17807 50.067023,28.8724 -15.038249,8.68226 -35.028768,-21.3657 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666cc;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 136.19445,144.4429 0.0455,20.67266 -78.028381,44.11611 -0.0031,-19.78119 z"
|
||||
style="opacity:0.9;fill:#6666af;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 127.13617,146.73547 0.0374,16.98921 -64.125308,36.25552 -0.0025,-16.25659 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.471179,178.6501 18.737341,10.818 0.0031,19.78099 -18.740409,-10.88245 z"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 47.647019,174.84764 15.398727,8.89046 0.0025,16.25641 -15.401249,-8.94341 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#b4e2ff;fill-rule:evenodd;stroke:none;stroke-width:1.00546169px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 56.43263,98.242186 -17.391087,10.041014 61.186527,35.32618 -61.020778,35.23005 18.839694,10.87703 61.020784,-35.23005 17.39108,-10.04102 z"
|
||||
style="opacity:0.9;fill:#9dbef0;fill-rule:evenodd;stroke:none;stroke-width:0.82630885px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 61.586284,108.76679 -14.292349,8.25191 50.284331,29.03177 -50.148115,28.95277 15.482843,8.93896 50.148116,-28.95277 14.29235,-8.25191 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
@@ -9,7 +9,7 @@
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "4.0.0",
|
||||
"css-loader": "0.28.0",
|
||||
"electron": "1.6.11",
|
||||
"electron": "1.8.4",
|
||||
"electron-builder": "17.1.1",
|
||||
"electron-builder-squirrel-windows": "17.0.1",
|
||||
"electron-rebuild": "1.5.11",
|
||||
@@ -98,7 +98,8 @@
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"screen"
|
||||
"screen",
|
||||
"gnome-python2-gnomekeyring"
|
||||
],
|
||||
"artifactName": "terminus-${version}-${os}-${arch}.rpm"
|
||||
}
|
||||
|
@@ -20,3 +20,14 @@ vars.builtinPlugins.forEach(plugin => {
|
||||
sh.exec(`${npx} yarn install`)
|
||||
sh.cd('..')
|
||||
})
|
||||
|
||||
if (['darwin', 'linux'].includes(process.platform)) {
|
||||
sh.cd('node_modules')
|
||||
for (let x of vars.builtinPlugins) {
|
||||
sh.ln('-fs', '../' + x, x)
|
||||
}
|
||||
for (let x of vars.bundledModules) {
|
||||
sh.ln('-fs', '../app/node_modules/' + x, x)
|
||||
}
|
||||
sh.cd('..')
|
||||
}
|
||||
|
@@ -16,5 +16,9 @@ exports.builtinPlugins = [
|
||||
'terminus-plugin-manager',
|
||||
'terminus-ssh',
|
||||
]
|
||||
exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
|
||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||
|
@@ -1,44 +1,54 @@
|
||||
!
|
||||
! Generated with :
|
||||
! XRDB2Xreources.py
|
||||
!
|
||||
*.foreground: #d8d8d8
|
||||
*.background: #181818
|
||||
*.cursorColor: #d8d8d8
|
||||
!
|
||||
! Black
|
||||
*.color0: #181818
|
||||
*.color8: #585858
|
||||
!
|
||||
! Red
|
||||
*.color1: #ab4642
|
||||
*.color9: #ab4642
|
||||
!
|
||||
! Green
|
||||
*.color2: #a1b56c
|
||||
*.color10: #a1b56c
|
||||
!
|
||||
! Yellow
|
||||
*.color3: #f7ca88
|
||||
*.color11: #f7ca88
|
||||
!
|
||||
! Blue
|
||||
*.color4: #7cafc2
|
||||
*.color12: #7cafc2
|
||||
!
|
||||
! Magenta
|
||||
*.color5: #ba8baf
|
||||
*.color13: #ba8baf
|
||||
!
|
||||
! Cyan
|
||||
*.color6: #86c1b9
|
||||
*.color14: #86c1b9
|
||||
!
|
||||
! White
|
||||
*.color7: #d8d8d8
|
||||
*.color15: #f8f8f8
|
||||
!
|
||||
! Bold, Italic, Underline
|
||||
*.colorBD: #d8d8d8
|
||||
!*.colorIT:
|
||||
!*.colorUL:
|
||||
! Base16 Default Dark
|
||||
! Scheme: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
#define base00 #181818
|
||||
#define base01 #282828
|
||||
#define base02 #383838
|
||||
#define base03 #585858
|
||||
#define base04 #b8b8b8
|
||||
#define base05 #d8d8d8
|
||||
#define base06 #e8e8e8
|
||||
#define base07 #f8f8f8
|
||||
#define base08 #ab4642
|
||||
#define base09 #dc9656
|
||||
#define base0A #f7ca88
|
||||
#define base0B #a1b56c
|
||||
#define base0C #86c1b9
|
||||
#define base0D #7cafc2
|
||||
#define base0E #ba8baf
|
||||
#define base0F #a16946
|
||||
|
||||
*.foreground: base05
|
||||
#ifdef background_opacity
|
||||
*.background: [background_opacity]base00
|
||||
#else
|
||||
*.background: base00
|
||||
#endif
|
||||
*.cursorColor: base05
|
||||
|
||||
*.color0: base00
|
||||
*.color1: base08
|
||||
*.color2: base0B
|
||||
*.color3: base0A
|
||||
*.color4: base0D
|
||||
*.color5: base0E
|
||||
*.color6: base0C
|
||||
*.color7: base05
|
||||
|
||||
*.color8: base03
|
||||
*.color9: base08
|
||||
*.color10: base0B
|
||||
*.color11: base0A
|
||||
*.color12: base0D
|
||||
*.color13: base0E
|
||||
*.color14: base0C
|
||||
*.color15: base07
|
||||
|
||||
! Note: colors beyond 15 might not be loaded (e.g., xterm, urxvt),
|
||||
! use 'shell' template to set these if necessary
|
||||
*.color16: base09
|
||||
*.color17: base0F
|
||||
*.color18: base01
|
||||
*.color19: base02
|
||||
*.color20: base04
|
||||
*.color21: base06
|
||||
|
@@ -10,13 +10,23 @@ export class ColorSchemes extends TerminalColorSchemeProvider {
|
||||
|
||||
schemeContents.keys().forEach(schemeFile => {
|
||||
let lines = (schemeContents(schemeFile) as string).split('\n')
|
||||
|
||||
// process #define variables
|
||||
let variables: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('#define'))
|
||||
.map(x => x.split(' ').map(v => v.trim()))
|
||||
.forEach(([ignore, variableName, variableValue]) => {
|
||||
variables[variableName] = variableValue
|
||||
})
|
||||
|
||||
let values: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('*.'))
|
||||
.map(x => x.substring(2))
|
||||
.map(x => x.split(':').map(v => v.trim()))
|
||||
.forEach(([key, value]) => {
|
||||
values[key] = value
|
||||
values[key] = variables[value] ? variables[value] : value
|
||||
})
|
||||
|
||||
let colors: string[] = []
|
||||
|
@@ -1,12 +1,13 @@
|
||||
title-bar(
|
||||
*ngIf='config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
*ngIf='!hostApp.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
[class.inset]='hostApp.platform == Platform.macOS'
|
||||
)
|
||||
|
||||
.content(
|
||||
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top"'
|
||||
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top"'
|
||||
)
|
||||
.tab-bar(
|
||||
*ngIf='!hostApp.isFullScreen',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"'
|
||||
)
|
||||
.tabs
|
||||
@@ -20,7 +21,7 @@ title-bar(
|
||||
@animateTab,
|
||||
(click)='app.selectTab(tab)',
|
||||
)
|
||||
|
||||
|
||||
.btn-group
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of leftToolbarButtons',
|
||||
@@ -28,9 +29,9 @@ title-bar(
|
||||
(click)='button.click()',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
|
||||
.drag-space(*ngIf='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
|
||||
.drag-space([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
.btn-group
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of rightToolbarButtons',
|
||||
@@ -53,7 +54,7 @@ title-bar(
|
||||
start-page(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body(
|
||||
*ngFor='let tab of app.tabs; trackBy: tab?.id',
|
||||
*ngFor='let tab of app.tabs; trackBy: tab?.id',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
[scrollable]='tab.scrollable',
|
||||
|
@@ -7,6 +7,7 @@
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
will-change: transform;
|
||||
cursor: default;
|
||||
animation: 0.5s ease-out fadeIn;
|
||||
}
|
||||
@@ -55,15 +56,20 @@ $tab-border-radius: 4px;
|
||||
}
|
||||
|
||||
&>.drag-space {
|
||||
min-width: 100px;
|
||||
flex: 1 0 25%;
|
||||
min-width: 1px;
|
||||
flex: 1 0 1%;
|
||||
-webkit-app-region: drag;
|
||||
|
||||
&.persistent {
|
||||
min-width: 100px;
|
||||
flex: 1 0 25%;
|
||||
}
|
||||
}
|
||||
|
||||
&.inset {
|
||||
padding-left: 85px;
|
||||
}
|
||||
|
||||
|
||||
window-controls {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import { DockingService } from '../services/docking.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
import { ThemesService } from '../services/themes.service'
|
||||
import { UpdaterService, Update } from '../services/updater.service'
|
||||
import { TouchbarService } from '../services/touchbar.service'
|
||||
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
@@ -62,6 +63,7 @@ export class AppRootComponent {
|
||||
private tabRecovery: TabRecoveryService,
|
||||
private hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
private touchbar: TouchbarService,
|
||||
public hostApp: HostAppService,
|
||||
public config: ConfigService,
|
||||
public app: AppService,
|
||||
@@ -97,6 +99,9 @@ export class AppRootComponent {
|
||||
this.app.previousTab()
|
||||
}
|
||||
}
|
||||
if (hotkey === 'toggle-fullscreen') {
|
||||
this.hostApp.toggleFullscreen()
|
||||
}
|
||||
})
|
||||
|
||||
this.docking.dock()
|
||||
@@ -118,16 +123,20 @@ export class AppRootComponent {
|
||||
this.updater.check().then(update => {
|
||||
this.appUpdate = update
|
||||
})
|
||||
|
||||
this.touchbar.update()
|
||||
}
|
||||
|
||||
onGlobalHotkey () {
|
||||
if (this.electron.app.window.isFocused()) {
|
||||
// focused
|
||||
this.electron.loseFocus()
|
||||
this.electron.app.window.hide()
|
||||
} else {
|
||||
if (!this.electron.app.window.isVisible()) {
|
||||
// unfocused, invisible
|
||||
this.electron.app.window.show()
|
||||
this.electron.app.window.focus()
|
||||
} else {
|
||||
if (this.config.store.appearance.dock === 'off') {
|
||||
// not docked, visible
|
||||
|
@@ -5,6 +5,7 @@ export abstract class BaseTabComponent {
|
||||
private static lastTabID = 0
|
||||
id: number
|
||||
title: string
|
||||
titleChange$ = new Subject<string>()
|
||||
customTitle: string
|
||||
scrollable: boolean
|
||||
hasActivity = false
|
||||
@@ -23,6 +24,13 @@ export abstract class BaseTabComponent {
|
||||
})
|
||||
}
|
||||
|
||||
setTitle (title: string) {
|
||||
this.title = title
|
||||
if (!this.customTitle) {
|
||||
this.titleChange$.next(title)
|
||||
}
|
||||
}
|
||||
|
||||
displayActivity (): void {
|
||||
this.hasActivity = true
|
||||
}
|
||||
|
@@ -14,8 +14,6 @@ $tabs-height: 36px;
|
||||
|
||||
transition: 0.125s ease-out all;
|
||||
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
.index {
|
||||
flex: none;
|
||||
font-weight: bold;
|
||||
|
@@ -69,6 +69,7 @@ export class TabHeaderComponent {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
modal.result.then(result => {
|
||||
this.tab.setTitle(result)
|
||||
this.tab.customTitle = result
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
hotkeys:
|
||||
toggle-window:
|
||||
- 'Ctrl+Space'
|
||||
toggle-fullscreen:
|
||||
- 'F11'
|
||||
close-tab:
|
||||
- 'Ctrl-Shift-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
|
@@ -1,48 +1,33 @@
|
||||
hotkeys:
|
||||
toggle-window:
|
||||
- 'Ctrl+Space'
|
||||
toggle-fullscreen:
|
||||
- 'Ctrl+⌘+F'
|
||||
close-tab:
|
||||
- '⌘-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
toggle-last-tab:
|
||||
- ['Ctrl-A', 'A']
|
||||
- ['Ctrl-A', 'Ctrl-A']
|
||||
toggle-last-tab: []
|
||||
next-tab:
|
||||
- '⌘-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- '⌘-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- '⌘-1'
|
||||
- ['Ctrl-A', '1']
|
||||
tab-2:
|
||||
- '⌘-2'
|
||||
- ['Ctrl-A', '2']
|
||||
tab-3:
|
||||
- '⌘-3'
|
||||
- ['Ctrl-A', '3']
|
||||
tab-4:
|
||||
- '⌘-4'
|
||||
- ['Ctrl-A', '4']
|
||||
tab-5:
|
||||
- '⌘-5'
|
||||
- ['Ctrl-A', '5']
|
||||
tab-6:
|
||||
- '⌘-6'
|
||||
- ['Ctrl-A', '6']
|
||||
tab-7:
|
||||
- '⌘-7'
|
||||
- ['Ctrl-A', '7']
|
||||
tab-8:
|
||||
- '⌘-8'
|
||||
- ['Ctrl-A', '8']
|
||||
tab-9:
|
||||
- '⌘-9'
|
||||
- ['Ctrl-A', '9']
|
||||
tab-10:
|
||||
- '⌘-0'
|
||||
- ['Ctrl-A', '0']
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -1,6 +1,8 @@
|
||||
hotkeys:
|
||||
toggle-window:
|
||||
- 'Ctrl+Space'
|
||||
toggle-fullscreen:
|
||||
- 'F11'
|
||||
close-tab:
|
||||
- 'Ctrl-Shift-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
|
@@ -3,6 +3,7 @@ appearance:
|
||||
dockScreen: current
|
||||
dockFill: 50
|
||||
tabsLocation: top
|
||||
cycleTabs: true
|
||||
theme: Standard
|
||||
frame: thin
|
||||
css: '/* * { color: blue !important; } */'
|
||||
|
@@ -14,6 +14,7 @@ import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
|
||||
import { DockingService } from './services/docking.service'
|
||||
import { TabRecoveryService } from './services/tabRecovery.service'
|
||||
import { ThemesService } from './services/themes.service'
|
||||
import { TouchbarService } from './services/touchbar.service'
|
||||
import { UpdaterService } from './services/updater.service'
|
||||
|
||||
import { AppRootComponent } from './components/appRoot.component'
|
||||
@@ -44,6 +45,7 @@ const PROVIDERS = [
|
||||
LogService,
|
||||
TabRecoveryService,
|
||||
ThemesService,
|
||||
TouchbarService,
|
||||
UpdaterService,
|
||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||
{ provide: Theme, useClass: StandardTheme, multi: true },
|
||||
|
@@ -2,7 +2,8 @@ import { Subject, AsyncSubject } from 'rxjs'
|
||||
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
|
||||
import { DefaultTabProvider } from '../api/defaultTabProvider'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
|
||||
@@ -10,14 +11,18 @@ export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
export class AppService {
|
||||
tabs: BaseTabComponent[] = []
|
||||
activeTab: BaseTabComponent
|
||||
activeTabChange$ = new Subject<BaseTabComponent>()
|
||||
lastTabIndex = 0
|
||||
logger: Logger
|
||||
tabsChanged$ = new Subject<void>()
|
||||
tabOpened$ = new Subject<BaseTabComponent>()
|
||||
tabClosed$ = new Subject<BaseTabComponent>()
|
||||
ready$ = new AsyncSubject<void>()
|
||||
|
||||
constructor (
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Optional() private defaultTabProvider: DefaultTabProvider,
|
||||
private config: ConfigService,
|
||||
private injector: Injector,
|
||||
log: LogService,
|
||||
) {
|
||||
@@ -33,6 +38,7 @@ export class AppService {
|
||||
this.tabs.push(componentRef.instance)
|
||||
this.selectTab(componentRef.instance)
|
||||
this.tabsChanged$.next()
|
||||
this.tabOpened$.next(componentRef.instance)
|
||||
|
||||
return componentRef.instance
|
||||
}
|
||||
@@ -57,6 +63,7 @@ export class AppService {
|
||||
this.activeTab.blurred$.next()
|
||||
}
|
||||
this.activeTab = tab
|
||||
this.activeTabChange$.next(tab)
|
||||
if (this.activeTab) {
|
||||
this.activeTab.focused$.next()
|
||||
}
|
||||
@@ -70,16 +77,24 @@ export class AppService {
|
||||
}
|
||||
|
||||
nextTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
if (this.tabs.length > 1) {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.selectTab(this.tabs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
if (this.tabs.length > 1) {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.selectTab(this.tabs[this.tabs.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +112,7 @@ export class AppService {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged$.next()
|
||||
this.tabClosed$.next(tab)
|
||||
}
|
||||
|
||||
emitReady () {
|
||||
|
@@ -29,18 +29,19 @@ export class DockingService {
|
||||
let dockSide = this.config.store.appearance.dock
|
||||
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
||||
let fill = this.config.store.appearance.dockFill
|
||||
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
|
||||
|
||||
if (dockSide === 'off') {
|
||||
this.hostApp.setAlwaysOnTop(false)
|
||||
return
|
||||
}
|
||||
if (dockSide === 'left' || dockSide === 'right') {
|
||||
newBounds.width = Math.round(fill * display.bounds.width)
|
||||
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
|
||||
newBounds.height = display.bounds.height
|
||||
}
|
||||
if (dockSide === 'top' || dockSide === 'bottom') {
|
||||
newBounds.width = display.bounds.width
|
||||
newBounds.height = Math.round(fill * display.bounds.height)
|
||||
newBounds.height = Math.max(minHeight, Math.round(fill * display.bounds.height))
|
||||
}
|
||||
if (dockSide === 'right') {
|
||||
newBounds.x = display.bounds.x + display.bounds.width - newBounds.width
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TouchBar } from 'electron'
|
||||
|
||||
@Injectable()
|
||||
export class ElectronService {
|
||||
@@ -10,6 +11,7 @@ export class ElectronService {
|
||||
globalShortcut: any
|
||||
screen: any
|
||||
remote: any
|
||||
TouchBar: typeof TouchBar
|
||||
private electron: any
|
||||
|
||||
constructor () {
|
||||
@@ -22,6 +24,7 @@ export class ElectronService {
|
||||
this.clipboard = this.electron.clipboard
|
||||
this.ipcRenderer = this.electron.ipcRenderer
|
||||
this.globalShortcut = this.remote.globalShortcut
|
||||
this.TouchBar = this.remote.TouchBar
|
||||
}
|
||||
|
||||
remoteRequire (name: string): any {
|
||||
@@ -29,6 +32,16 @@ export class ElectronService {
|
||||
}
|
||||
|
||||
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
|
||||
return this.remoteRequire(globals.require.resolve(`${plugin}/node_modules/${module}`))
|
||||
return this.remoteRequire(this.remoteResolvePluginModule(plugin, module, globals))
|
||||
}
|
||||
|
||||
remoteResolvePluginModule (plugin: string, module: string, globals: any): any {
|
||||
return globals.require.resolve(`${plugin}/node_modules/${module}`)
|
||||
}
|
||||
|
||||
loseFocus () {
|
||||
if (process.platform === 'darwin') {
|
||||
this.remote.Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ export class HostAppService {
|
||||
ready = new EventEmitter<any>()
|
||||
shown = new EventEmitter<any>()
|
||||
secondInstance$ = new Subject<{ argv: string[], cwd: string }>()
|
||||
|
||||
isFullScreen = false
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
@@ -44,6 +44,14 @@ export class HostAppService {
|
||||
this.logger.error('Unhandled exception:', err)
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:window-enter-full-screen', () => this.zone.run(() => {
|
||||
this.isFullScreen = true
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-leave-full-screen', () => this.zone.run(() => {
|
||||
this.isFullScreen = false
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-shown', () => {
|
||||
this.zone.run(() => this.shown.emit())
|
||||
})
|
||||
@@ -73,18 +81,19 @@ export class HostAppService {
|
||||
return this.electron.app.getPath(type)
|
||||
}
|
||||
|
||||
toggleFullscreen () {
|
||||
let window = this.getWindow()
|
||||
window.setFullScreen(!window.isFullScreen())
|
||||
}
|
||||
|
||||
openDevTools () {
|
||||
this.getWindow().webContents.openDevTools()
|
||||
this.getWindow().webContents.openDevTools({ mode: 'undocked' })
|
||||
}
|
||||
|
||||
focusWindow () {
|
||||
this.electron.ipcRenderer.send('window-focus')
|
||||
}
|
||||
|
||||
toggleWindow () {
|
||||
this.electron.ipcRenderer.send('window-toggle-focus')
|
||||
}
|
||||
|
||||
minimize () {
|
||||
this.electron.ipcRenderer.send('window-minimize')
|
||||
}
|
||||
|
@@ -178,6 +178,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
id: 'toggle-window',
|
||||
name: 'Toggle terminal window',
|
||||
},
|
||||
{
|
||||
id: 'toggle-fullscreen',
|
||||
name: 'Toggle fullscreen mode',
|
||||
},
|
||||
{
|
||||
id: 'close-tab',
|
||||
name: 'Close tab',
|
||||
|
@@ -5,7 +5,7 @@ export const metaKeyName = {
|
||||
}[process.platform]
|
||||
|
||||
export const altKeyName = {
|
||||
darwin: 'Option',
|
||||
darwin: '⌥',
|
||||
win32: 'Alt',
|
||||
linux: 'Alt',
|
||||
}[process.platform]
|
||||
|
@@ -41,7 +41,9 @@ export class Logger {
|
||||
|
||||
doLog (level: string, ...args: any[]) {
|
||||
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
|
||||
this.winstonLogger[level](...args)
|
||||
if (this.winstonLogger) {
|
||||
this.winstonLogger[level](...args)
|
||||
}
|
||||
}
|
||||
|
||||
debug (...args: any[]) { this.doLog('debug', ...args) }
|
||||
|
70
terminus-core/src/services/touchbar.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Injectable, Inject, NgZone } from '@angular/core'
|
||||
import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { AppService } from './app.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class TouchbarService {
|
||||
tabSelected$ = new Subject<number>()
|
||||
private titleSubscriptions = new Map<BaseTabComponent, Subscription>()
|
||||
private tabsSegmentedControl: TouchBarSegmentedControl
|
||||
private tabSegments: SegmentedControlSegment[] = []
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
private config: ConfigService,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
app.tabsChanged$.subscribe(() => this.update())
|
||||
app.activeTabChange$.subscribe(() => this.update())
|
||||
app.tabOpened$.subscribe(tab => {
|
||||
let sub = tab.titleChange$.subscribe(title => {
|
||||
this.tabSegments[app.tabs.indexOf(tab)].label = title
|
||||
this.tabsSegmentedControl.segments = this.tabSegments
|
||||
})
|
||||
this.titleSubscriptions.set(tab, sub)
|
||||
})
|
||||
app.tabClosed$.subscribe(tab => {
|
||||
this.titleSubscriptions.get(tab).unsubscribe()
|
||||
this.titleSubscriptions.delete(tab)
|
||||
})
|
||||
}
|
||||
|
||||
update () {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
buttons.sort((a, b) => (a.weight || 0) - (b.weight || 0))
|
||||
this.tabSegments = this.app.tabs.map(tab => ({
|
||||
label: tab.title,
|
||||
}))
|
||||
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
|
||||
segments: this.tabSegments,
|
||||
selectedIndex: this.app.tabs.indexOf(this.app.activeTab),
|
||||
change: (selectedIndex) => this.zone.run(() => {
|
||||
this.app.selectTab(this.app.tabs[selectedIndex])
|
||||
})
|
||||
})
|
||||
let touchBar = new this.electron.TouchBar({
|
||||
items: [
|
||||
this.tabsSegmentedControl,
|
||||
new this.electron.TouchBar.TouchBarSpacer({size: 'flexible'}),
|
||||
new this.electron.TouchBar.TouchBarSpacer({size: 'small'}),
|
||||
...buttons.map(button => new this.electron.TouchBar.TouchBarButton({
|
||||
label: button.title,
|
||||
// backgroundColor: '#0022cc',
|
||||
click: () => this.zone.run(() => button.click()),
|
||||
}))
|
||||
]
|
||||
})
|
||||
this.electron.app.window.setTouchBar(touchBar)
|
||||
}
|
||||
|
||||
}
|
@@ -105,7 +105,7 @@ window-controls {
|
||||
}
|
||||
}
|
||||
|
||||
$border-color: #141414;
|
||||
$border-color: #111;
|
||||
|
||||
app-root {
|
||||
&> .content {
|
||||
@@ -131,7 +131,7 @@ app-root {
|
||||
background: $body-bg2;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-top: 1px solid transparent;
|
||||
transition: 0.25s all;
|
||||
|
||||
.index {
|
||||
color: #555;
|
||||
@@ -159,17 +159,15 @@ app-root {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
tab-header {
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: -1px;
|
||||
|
||||
&.active {
|
||||
border-top: 1px solid $teal;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
border-top: 1px solid $green;
|
||||
background: linear-gradient(to bottom, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,17 +176,15 @@ app-root {
|
||||
border-top: 1px solid $border-color;
|
||||
|
||||
tab-header {
|
||||
border-bottom: 1px solid transparent;
|
||||
border-top: 1px solid $border-color;
|
||||
margin-top: -1px;
|
||||
|
||||
&.active {
|
||||
border-bottom: 1px solid $teal;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
border-bottom: 1px solid $green;
|
||||
background: linear-gradient(to top, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ export class PluginManagerService {
|
||||
return
|
||||
}
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
let searchPaths = (await exec('bash -c -l "echo $PATH"'))[0].toString().trim().split(':')
|
||||
let searchPaths = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim().split(':')
|
||||
for (let searchPath of searchPaths) {
|
||||
if (await fs.exists(path.join(searchPath, 'npm'))) {
|
||||
this.logger.debug('Found npm in', searchPath)
|
||||
@@ -69,7 +69,11 @@ export class PluginManagerService {
|
||||
listAvailable (query?: string): Observable<IPluginInfo[]> {
|
||||
return Observable
|
||||
.fromPromise(
|
||||
axios.get(`https://www.npmjs.com/-/search?text=keywords:${KEYWORD}+${encodeURIComponent(query || '')}&from=0&size=1000`)
|
||||
axios.get(`https://www.npmjs.com/search?q=keywords%3A${KEYWORD}+${encodeURIComponent(query || '')}&from=0&size=1000`, {
|
||||
headers: {
|
||||
'x-spiferack': '1',
|
||||
}
|
||||
})
|
||||
)
|
||||
.map(response => response.data.objects.map(item => ({
|
||||
name: item.package.name.substring(NAME_PREFIX.length),
|
||||
|
@@ -28,7 +28,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
) {
|
||||
super()
|
||||
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.title = 'Settings'
|
||||
this.setTitle('Settings')
|
||||
this.scrollable = true
|
||||
this.screens = this.docking.getScreens()
|
||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||
|
@@ -22,6 +22,7 @@
|
||||
"apply-loader": "^2.0.0",
|
||||
"awesome-typescript-loader": "^3.1.2",
|
||||
"electron": "^1.6.11",
|
||||
"ngx-toastr": "^8.0.0",
|
||||
"pug": "^2.0.0-rc.3",
|
||||
"pug-loader": "^2.3.0",
|
||||
"rxjs": "^5.4.0",
|
||||
|
@@ -3,6 +3,7 @@ import { BaseSession } from 'terminus-terminal'
|
||||
export interface SSHConnection {
|
||||
name?: string
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
password?: string
|
||||
privateKey?: string
|
||||
|
@@ -18,17 +18,14 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
}
|
||||
|
||||
activate () {
|
||||
let modal = this.ngbModal.open(SSHModalComponent)
|
||||
modal.result.then(() => {
|
||||
//this.terminal.openTab(shell)
|
||||
})
|
||||
this.ngbModal.open(SSHModalComponent)
|
||||
}
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'globe',
|
||||
weight: 5,
|
||||
title: 'SSH connections',
|
||||
title: 'SSH',
|
||||
click: async () => {
|
||||
this.activate()
|
||||
}
|
||||
|
@@ -13,6 +13,14 @@
|
||||
[(ngModel)]='connection.host',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Port
|
||||
input.form-control(
|
||||
type='number',
|
||||
placeholder='22',
|
||||
[(ngModel)]='connection.port',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Username
|
||||
input.form-control(
|
||||
|
@@ -3,7 +3,7 @@
|
||||
type='text',
|
||||
[(ngModel)]='quickTarget',
|
||||
autofocus,
|
||||
placeholder='Quick connect: [user@]host',
|
||||
placeholder='Quick connect: [user@]host[:port]',
|
||||
(keyup.enter)='quickConnect()'
|
||||
)
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { ConfigService, AppService } from 'terminus-core'
|
||||
import { SettingsTabComponent } from 'terminus-settings'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
@@ -19,6 +20,7 @@ export class SSHModalComponent {
|
||||
private config: ConfigService,
|
||||
private ssh: SSHService,
|
||||
private app: AppService,
|
||||
private toastr: ToastrService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
@@ -31,12 +33,18 @@ export class SSHModalComponent {
|
||||
quickConnect () {
|
||||
let user = 'root'
|
||||
let host = this.quickTarget
|
||||
let port = 22
|
||||
if (host.includes('@')) {
|
||||
[user, host] = host.split('@')
|
||||
}
|
||||
if (host.includes(':')) {
|
||||
port = parseInt(host.split(':')[1])
|
||||
host = host.split(':')[0]
|
||||
}
|
||||
|
||||
let connection: SSHConnection = {
|
||||
name: this.quickTarget,
|
||||
host, user,
|
||||
host, user, port
|
||||
}
|
||||
window.localStorage.lastConnection = JSON.stringify(connection)
|
||||
this.connect(connection)
|
||||
@@ -45,7 +53,7 @@ export class SSHModalComponent {
|
||||
connect (connection: SSHConnection) {
|
||||
this.close()
|
||||
this.ssh.connect(connection).catch(error => {
|
||||
alert(`Could not connect: ${error}`)
|
||||
this.toastr.error(`Could not connect: ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -21,6 +21,7 @@ export class SSHSettingsTabComponent {
|
||||
let connection: SSHConnection = {
|
||||
name: '',
|
||||
host: '',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
}
|
||||
let modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||
|
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
import { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
@@ -10,6 +11,7 @@ import { SSHModalComponent } from './components/sshModal.component'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
import { SSHService } from './services/ssh.service'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SSHConfigProvider } from './config'
|
||||
@@ -20,8 +22,10 @@ import { SSHSettingsTabProvider } from './settings'
|
||||
NgbModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ToastrModule,
|
||||
],
|
||||
providers: [
|
||||
PasswordStorageService,
|
||||
SSHService,
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
||||
|
73
terminus-ssh/src/services/passwordStorage.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { SSHConnection } from '../api'
|
||||
|
||||
let xkeychain
|
||||
let wincredmgr
|
||||
try {
|
||||
xkeychain = require('xkeychain')
|
||||
} catch (error) {
|
||||
try {
|
||||
wincredmgr = require('wincredmgr')
|
||||
} catch (error2) {
|
||||
console.warn('No keychain manager available')
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PasswordStorageService {
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
) { }
|
||||
|
||||
savePassword (connection: SSHConnection, password: string) {
|
||||
if (xkeychain) {
|
||||
xkeychain.setPassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
password
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.WriteCredentials(
|
||||
'user',
|
||||
password,
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deletePassword (connection: SSHConnection) {
|
||||
if (xkeychain) {
|
||||
xkeychain.deletePassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.DeleteCredentials(
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadPassword (connection: SSHConnection): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
if (!wincredmgr && !xkeychain.isSupported()) {
|
||||
return resolve(null)
|
||||
}
|
||||
if (xkeychain) {
|
||||
xkeychain.getPassword(
|
||||
{
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
},
|
||||
(_, result) => this.zone.run(() => resolve(result))
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
|
||||
} catch (error) {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -3,92 +3,61 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { AppService, HostAppService, Platform } from 'terminus-core'
|
||||
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 { PasswordStorageService } from './passwordStorage.service'
|
||||
const { SSH2Stream } = require('ssh2-streams')
|
||||
|
||||
let xkeychain
|
||||
let wincredmgr
|
||||
try {
|
||||
xkeychain = require('xkeychain')
|
||||
} catch (error) {
|
||||
try {
|
||||
wincredmgr = require('wincredmgr')
|
||||
} catch (error2) {
|
||||
console.warn('No keychain manager available')
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SSHService {
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
private app: AppService,
|
||||
private zone: NgZone,
|
||||
private ngbModal: NgbModal,
|
||||
private hostApp: HostAppService,
|
||||
private passwordStorage: PasswordStorageService,
|
||||
private toastr: ToastrService,
|
||||
) {
|
||||
}
|
||||
|
||||
savePassword (connection: SSHConnection, password: string) {
|
||||
if (xkeychain) {
|
||||
xkeychain.setPassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
password
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.WriteCredentials(
|
||||
'user',
|
||||
password,
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deletePassword (connection: SSHConnection) {
|
||||
if (xkeychain) {
|
||||
xkeychain.deletePassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.DeleteCredentials(
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadPassword (connection: SSHConnection): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
if (xkeychain) {
|
||||
xkeychain.getPassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
}, (_, result) => resolve(result))
|
||||
} else {
|
||||
try {
|
||||
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
|
||||
} catch (error) {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.logger = log.create('ssh')
|
||||
}
|
||||
|
||||
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
||||
let privateKey: string = null
|
||||
let keyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
||||
if (!connection.privateKey && await fs.exists(keyPath)) {
|
||||
connection.privateKey = keyPath
|
||||
let privateKeyPassphrase: string = null
|
||||
let privateKeyPath = connection.privateKey
|
||||
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)
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (connection.privateKey) {
|
||||
if (privateKeyPath) {
|
||||
try {
|
||||
privateKey = (await fs.readFile(connection.privateKey)).toString()
|
||||
} catch (error) { }
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
this.toastr.warning('Could not read the private key file')
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
this.logger.info('Loaded private key from', privateKeyPath)
|
||||
|
||||
if (privateKey.includes('ENCRYPTED')) {
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
try {
|
||||
privateKeyPassphrase = await modal.result
|
||||
} catch (_err) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssh = new Client()
|
||||
@@ -98,15 +67,15 @@ export class SSHService {
|
||||
ssh.on('ready', () => {
|
||||
connected = true
|
||||
if (savedPassword) {
|
||||
this.savePassword(connection, savedPassword)
|
||||
this.passwordStorage.savePassword(connection, savedPassword)
|
||||
}
|
||||
this.zone.run(resolve)
|
||||
})
|
||||
ssh.on('error', error => {
|
||||
this.deletePassword(connection)
|
||||
this.passwordStorage.deletePassword(connection)
|
||||
this.zone.run(() => {
|
||||
if (connected) {
|
||||
alert(`SSH error: ${error}`)
|
||||
this.toastr.error(error.toString())
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
@@ -133,9 +102,11 @@ export class SSHService {
|
||||
|
||||
ssh.connect({
|
||||
host: connection.host,
|
||||
port: connection.port || 22,
|
||||
username: connection.user,
|
||||
password: privateKey ? undefined : '',
|
||||
password: connection.privateKey ? undefined : '',
|
||||
privateKey,
|
||||
passphrase: privateKeyPassphrase,
|
||||
tryKeyboard: true,
|
||||
agent,
|
||||
agentForward: !!agent,
|
||||
@@ -145,12 +116,14 @@ export class SSHService {
|
||||
|
||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||
if (connection.password) {
|
||||
this.logger.info('Using preset password')
|
||||
return connection.password
|
||||
}
|
||||
|
||||
if (!keychainPasswordUsed && (wincredmgr || xkeychain.isSupported())) {
|
||||
let password = await this.loadPassword(connection)
|
||||
if (!keychainPasswordUsed) {
|
||||
let password = await this.passwordStorage.loadPassword(connection)
|
||||
if (password) {
|
||||
this.logger.info('Using saved password')
|
||||
keychainPasswordUsed = true
|
||||
return password
|
||||
}
|
||||
|
@@ -40,6 +40,7 @@ module.exports = {
|
||||
'xkeychain',
|
||||
'wincredmgr',
|
||||
'path',
|
||||
'ngx-toastr',
|
||||
/^rxjs/,
|
||||
/^@angular/,
|
||||
/^@ng-bootstrap/,
|
||||
|
@@ -2,9 +2,13 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@*", "@types/node@^8.0.24":
|
||||
version "8.0.53"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"
|
||||
"@types/node@*":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
|
||||
|
||||
"@types/node@^7.0.18":
|
||||
version "7.0.52"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.52.tgz#8990d3350375542b0c21a83cd0331e6a8fc86716"
|
||||
|
||||
"@types/ssh2-streams@*":
|
||||
version "0.1.2"
|
||||
@@ -20,8 +24,8 @@
|
||||
"@types/ssh2-streams" "*"
|
||||
|
||||
"@types/webpack-env@^1.13.0":
|
||||
version "1.13.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.2.tgz#c290b99dbef74df21b06671aea36e355bf3b27e1"
|
||||
version "1.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.3.tgz#0ecbe70f87341767793774d3683b51aa3246434c"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
@@ -48,8 +52,8 @@ acorn@^4.0.3, acorn@^4.0.4, acorn@~4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
|
||||
|
||||
acorn@^5.0.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
|
||||
|
||||
ajv-keywords@^1.1.1:
|
||||
version "1.5.1"
|
||||
@@ -63,8 +67,8 @@ ajv@^4.7.0, ajv@^4.9.1:
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.1.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9"
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
@@ -171,6 +175,10 @@ assert@^1.1.1:
|
||||
dependencies:
|
||||
util "0.10.3"
|
||||
|
||||
assign-symbols@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
|
||||
|
||||
async-each@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||
@@ -190,8 +198,8 @@ atob@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d"
|
||||
|
||||
awesome-typescript-loader@^3.1.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.4.0.tgz#aed2c83af614d617d11e3ec368ac3befb55d002f"
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.4.1.tgz#22fa49800f0619ec18ab15383aef93b95378dea9"
|
||||
dependencies:
|
||||
colors "^1.1.2"
|
||||
enhanced-resolve "3.3.0"
|
||||
@@ -456,13 +464,12 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
class-utils@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.5.tgz#17e793103750f9627b2176ea34cfd1b565903c80"
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
|
||||
dependencies:
|
||||
arr-union "^3.1.0"
|
||||
define-property "^0.2.5"
|
||||
isobject "^3.0.0"
|
||||
lazy-cache "^2.0.2"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
clean-css@^3.3.0:
|
||||
@@ -721,10 +728,10 @@ electron-download@^3.0.1:
|
||||
sumchecker "^1.2.0"
|
||||
|
||||
electron@^1.6.11:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.1.tgz#19b6f39f2013e204a91a60bc3086dc7a4a07ed88"
|
||||
version "1.7.10"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.10.tgz#3a3e83d965fd7fafe473be8ddf8f472561b6253d"
|
||||
dependencies:
|
||||
"@types/node" "^8.0.24"
|
||||
"@types/node" "^7.0.18"
|
||||
electron-download "^3.0.1"
|
||||
extract-zip "^1.0.3"
|
||||
|
||||
@@ -744,7 +751,7 @@ emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
|
||||
enhanced-resolve@3.3.0, enhanced-resolve@^3.3.0:
|
||||
enhanced-resolve@3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3"
|
||||
dependencies:
|
||||
@@ -753,11 +760,20 @@ enhanced-resolve@3.3.0, enhanced-resolve@^3.3.0:
|
||||
object-assign "^4.0.1"
|
||||
tapable "^0.2.5"
|
||||
|
||||
errno@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
|
||||
enhanced-resolve@^3.3.0:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
|
||||
dependencies:
|
||||
prr "~0.0.0"
|
||||
graceful-fs "^4.1.2"
|
||||
memory-fs "^0.4.0"
|
||||
object-assign "^4.0.1"
|
||||
tapable "^0.2.7"
|
||||
|
||||
errno@^0.1.3:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
|
||||
dependencies:
|
||||
prr "~1.0.1"
|
||||
|
||||
error-ex@^1.2.0:
|
||||
version "1.3.1"
|
||||
@@ -766,8 +782,8 @@ error-ex@^1.2.0:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es6-promise@^4.0.5:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a"
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.2.tgz#f722d7769af88bd33bc13ec6605e1f92966b82d9"
|
||||
|
||||
events@^1.0.0:
|
||||
version "1.1.1"
|
||||
@@ -811,9 +827,10 @@ extend-shallow@^2.0.1:
|
||||
is-extendable "^0.1.0"
|
||||
|
||||
extend-shallow@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.1.tgz#4b6d8c49b147fee029dc9eb9484adb770f689844"
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
|
||||
dependencies:
|
||||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@~3.0.0, extend@~3.0.1:
|
||||
@@ -827,8 +844,8 @@ extglob@^0.3.1:
|
||||
is-extglob "^1.0.0"
|
||||
|
||||
extglob@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.2.tgz#3290f46208db1b2e8eb8be0c94ed9e6ad80edbe2"
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
|
||||
dependencies:
|
||||
array-unique "^0.3.2"
|
||||
define-property "^1.0.0"
|
||||
@@ -848,10 +865,14 @@ extract-zip@^1.0.3:
|
||||
mkdirp "0.5.0"
|
||||
yauzl "2.4.1"
|
||||
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
@@ -1216,8 +1237,8 @@ ini@~1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
|
||||
interpret@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
||||
|
||||
invert-kv@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1229,6 +1250,12 @@ is-accessor-descriptor@^0.1.6:
|
||||
dependencies:
|
||||
kind-of "^3.0.2"
|
||||
|
||||
is-accessor-descriptor@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
|
||||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
@@ -1255,6 +1282,12 @@ is-data-descriptor@^0.1.4:
|
||||
dependencies:
|
||||
kind-of "^3.0.2"
|
||||
|
||||
is-data-descriptor@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
|
||||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-descriptor@^0.1.0:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
|
||||
@@ -1264,12 +1297,12 @@ is-descriptor@^0.1.0:
|
||||
kind-of "^5.0.0"
|
||||
|
||||
is-descriptor@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.1.tgz#2c6023599bde2de9d5d2c8b9a9d94082036b6ef2"
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
|
||||
dependencies:
|
||||
is-accessor-descriptor "^0.1.6"
|
||||
is-data-descriptor "^0.1.4"
|
||||
kind-of "^5.0.0"
|
||||
is-accessor-descriptor "^1.0.0"
|
||||
is-data-descriptor "^1.0.0"
|
||||
kind-of "^6.0.2"
|
||||
|
||||
is-dotfile@^1.0.0:
|
||||
version "1.0.3"
|
||||
@@ -1475,9 +1508,9 @@ kind-of@^5.0.0, kind-of@^5.0.2:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
|
||||
|
||||
kind-of@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.1.tgz#4948e6263553ac3712fc44d305b77851d9e40ea4"
|
||||
kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||
|
||||
klaw@^1.0.0:
|
||||
version "1.3.1"
|
||||
@@ -1609,8 +1642,8 @@ micromatch@^2.1.5:
|
||||
regex-cache "^0.4.2"
|
||||
|
||||
micromatch@^3.0.3:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.4.tgz#bb812e741a41f982c854e42b421a7eac458796f4"
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba"
|
||||
dependencies:
|
||||
arr-diff "^4.0.0"
|
||||
array-unique "^0.3.2"
|
||||
@@ -1666,11 +1699,11 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.2.0.tgz#d02b8c6f8b6d4b8f5982d3fd009c4919851c3fe2"
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a"
|
||||
dependencies:
|
||||
for-in "^1.0.2"
|
||||
is-extendable "^0.1.1"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
mkdirp@0.5.0:
|
||||
version "0.5.0"
|
||||
@@ -1693,8 +1726,8 @@ nan@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
|
||||
|
||||
nanomatch@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.5.tgz#5c9ab02475c76676275731b0bf0a7395c624a9c4"
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
|
||||
dependencies:
|
||||
arr-diff "^4.0.0"
|
||||
array-unique "^0.3.2"
|
||||
@@ -1708,6 +1741,10 @@ nanomatch@^1.2.5:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
ngx-toastr@^8.0.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-8.1.0.tgz#3a0742e62895f88e232607843d61373d6f0d44d3"
|
||||
|
||||
node-libs-browser@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
|
||||
@@ -2000,9 +2037,9 @@ promise@^7.0.1:
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prr@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
|
||||
prr@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
||||
|
||||
public-encrypt@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -2147,8 +2184,8 @@ randomatic@^1.1.3:
|
||||
kind-of "^4.0.0"
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
|
||||
dependencies:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
@@ -2160,8 +2197,8 @@ randomfill@^1.0.3:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
rc@^1.1.2, rc@^1.1.7:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077"
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3"
|
||||
dependencies:
|
||||
deep-extend "~0.4.0"
|
||||
ini "~1.3.0"
|
||||
@@ -2183,7 +2220,7 @@ read-pkg@^1.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^1.0.0"
|
||||
|
||||
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.3.3:
|
||||
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
|
||||
dependencies:
|
||||
@@ -2342,18 +2379,18 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
inherits "^2.0.1"
|
||||
|
||||
rxjs@^5.4.0:
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3"
|
||||
version "5.5.6"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02"
|
||||
dependencies:
|
||||
symbol-observable "^1.0.1"
|
||||
symbol-observable "1.0.1"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -2552,12 +2589,12 @@ stream-browserify@^2.0.1:
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
stream-http@^2.7.2:
|
||||
version "2.7.2"
|
||||
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
|
||||
dependencies:
|
||||
builtin-status-codes "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.2.6"
|
||||
readable-stream "^2.3.3"
|
||||
to-arraybuffer "^1.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
@@ -2622,11 +2659,11 @@ supports-color@^3.1.0:
|
||||
dependencies:
|
||||
has-flag "^1.0.0"
|
||||
|
||||
symbol-observable@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
|
||||
symbol-observable@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
||||
|
||||
tapable@^0.2.5, tapable@~0.2.5:
|
||||
tapable@^0.2.5, tapable@^0.2.7, tapable@~0.2.5:
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
|
||||
|
||||
@@ -2726,8 +2763,8 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
typescript@^2.2.2:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"
|
||||
|
||||
uglify-js@^2.6.1, uglify-js@^2.8.27:
|
||||
version "2.8.29"
|
||||
@@ -2792,8 +2829,8 @@ util@0.10.3, util@^0.10.3:
|
||||
inherits "2.0.1"
|
||||
|
||||
uuid@^3.0.0, uuid@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
@@ -16,7 +16,13 @@ export class HyperColorSchemes extends TerminalColorSchemeProvider {
|
||||
try {
|
||||
let module = (global as any).require(path.join(pluginsPath, plugin))
|
||||
if (module.decorateConfig) {
|
||||
let config = module.decorateConfig({})
|
||||
let config: any
|
||||
try {
|
||||
config = module.decorateConfig({})
|
||||
} catch (error) {
|
||||
console.warn('Could not load Hyper theme:', plugin)
|
||||
return
|
||||
}
|
||||
if (config.colors) {
|
||||
themes.push({
|
||||
name: plugin,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs'
|
||||
import 'rxjs/add/operator/bufferTime'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, ThemesService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
||||
|
||||
@@ -54,11 +54,12 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
private electron: ElectronService,
|
||||
private terminalService: TerminalService,
|
||||
public config: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
|
||||
) {
|
||||
super()
|
||||
this.decorators = this.decorators || []
|
||||
this.title = 'Terminal'
|
||||
this.setTitle('Terminal')
|
||||
this.resize$.first().subscribe(async (resizeEvent) => {
|
||||
if (!this.session) {
|
||||
this.session = this.sessions.addSession(
|
||||
@@ -88,20 +89,50 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
if (hotkey === 'copy') {
|
||||
switch (hotkey) {
|
||||
case 'ctrl-c':
|
||||
if (this.hterm.getSelectionText()) {
|
||||
this.hterm.copySelectionToClipboard()
|
||||
} else {
|
||||
this.sendInput('\x03')
|
||||
}
|
||||
break
|
||||
case 'copy':
|
||||
this.hterm.copySelectionToClipboard()
|
||||
}
|
||||
if (hotkey === 'clear') {
|
||||
break
|
||||
case 'paste':
|
||||
this.paste()
|
||||
break
|
||||
case 'clear':
|
||||
this.clear()
|
||||
}
|
||||
if (hotkey === 'zoom-in') {
|
||||
break
|
||||
case 'zoom-in':
|
||||
this.zoomIn()
|
||||
}
|
||||
if (hotkey === 'zoom-out') {
|
||||
break
|
||||
case 'zoom-out':
|
||||
this.zoomOut()
|
||||
}
|
||||
if (hotkey === 'reset-zoom') {
|
||||
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')
|
||||
@@ -191,11 +222,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
|
||||
attachHTermHandlers (hterm: any) {
|
||||
hterm.setWindowTitle = (title) => {
|
||||
this.zone.run(() => {
|
||||
this.title = title
|
||||
})
|
||||
}
|
||||
hterm.setWindowTitle = title => this.zone.run(() => this.setTitle(title))
|
||||
|
||||
const _setAlternateMode = hterm.setAlternateMode.bind(hterm)
|
||||
hterm.setAlternateMode = (state) => {
|
||||
@@ -203,15 +230,18 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.alternateScreenActive$.next(state)
|
||||
}
|
||||
|
||||
const _copySelectionToClipboard = hterm.copySelectionToClipboard.bind(hterm)
|
||||
hterm.copySelectionToClipboard = () => {
|
||||
_copySelectionToClipboard()
|
||||
this.toastr.info('Copied')
|
||||
}
|
||||
|
||||
hterm.primaryScreen_.syncSelectionCaret = () => null
|
||||
hterm.alternateScreen_.syncSelectionCaret = () => null
|
||||
hterm.primaryScreen_.terminal = hterm
|
||||
hterm.alternateScreen_.terminal = hterm
|
||||
|
||||
const _onPaste = hterm.scrollPort_.onPaste_.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.onPaste_ = (event) => {
|
||||
hterm.scrollPort_.pasteTarget_.value = event.clipboardData.getData('text/plain').trim()
|
||||
_onPaste()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
@@ -313,7 +343,12 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
|
||||
paste () {
|
||||
this.sendInput(this.electron.clipboard.readText())
|
||||
let data = this.electron.clipboard.readText()
|
||||
data = this.hterm.keyboard.encode(data)
|
||||
if (this.hterm.options_.bracketedPaste) {
|
||||
data = '\x1b[200~' + data + '\x1b[201~'
|
||||
}
|
||||
this.sendInput(data)
|
||||
}
|
||||
|
||||
clear () {
|
||||
@@ -321,7 +356,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.hterm.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
async configure (): Promise<void> {
|
||||
configure (): void {
|
||||
let config = this.config.store
|
||||
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
this.setFontSize()
|
||||
@@ -338,6 +373,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
|
||||
preferenceManager.set('pass-alt-number', true)
|
||||
preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
|
||||
preferenceManager.set('clear-selection-after-copy', true)
|
||||
|
||||
if (config.terminal.colorScheme.foreground) {
|
||||
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
|
||||
|
@@ -53,9 +53,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
persistence: 'screen',
|
||||
},
|
||||
hotkeys: {
|
||||
'ctrl-c': ['Ctrl-C'],
|
||||
'copy': [
|
||||
'⌘-C',
|
||||
],
|
||||
'paste': [
|
||||
'⌘-V',
|
||||
],
|
||||
'clear': [
|
||||
'⌘-K',
|
||||
],
|
||||
@@ -75,7 +79,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
['Ctrl-A', 'Ctrl-C'],
|
||||
'⌘-T',
|
||||
'⌘-N',
|
||||
]
|
||||
],
|
||||
'home': ['⌘-ArrowLeft', 'Home'],
|
||||
'end': ['⌘-ArrowRight', 'End'],
|
||||
'previous-word': ['⌥-ArrowLeft'],
|
||||
'next-word': ['⌥-ArrowRight'],
|
||||
'delete-previous-word': ['⌥-Backspace'],
|
||||
'delete-next-word': ['⌥-Delete'],
|
||||
},
|
||||
},
|
||||
[Platform.Windows]: {
|
||||
@@ -87,9 +97,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
copyOnSelect: true,
|
||||
},
|
||||
hotkeys: {
|
||||
'ctrl-c': ['Ctrl-C'],
|
||||
'copy': [
|
||||
'Ctrl-Shift-C',
|
||||
],
|
||||
'paste': [
|
||||
'Ctrl-Shift-V',
|
||||
],
|
||||
'clear': [
|
||||
'Ctrl-L',
|
||||
],
|
||||
@@ -108,7 +122,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
['Ctrl-A', 'C'],
|
||||
['Ctrl-A', 'Ctrl-C'],
|
||||
'Ctrl-Shift-T',
|
||||
]
|
||||
],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'previous-word': ['Ctrl-ArrowLeft'],
|
||||
'next-word': ['Ctrl-ArrowRight'],
|
||||
'delete-previous-word': ['Ctrl-Backspace'],
|
||||
'delete-next-word': ['Ctrl-Delete'],
|
||||
},
|
||||
},
|
||||
[Platform.Linux]: {
|
||||
@@ -118,9 +138,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
persistence: 'tmux',
|
||||
},
|
||||
hotkeys: {
|
||||
'ctrl-c': ['Ctrl-C'],
|
||||
'copy': [
|
||||
'Ctrl-Shift-C',
|
||||
],
|
||||
'paste': [
|
||||
'Ctrl-Shift-V',
|
||||
],
|
||||
'clear': [
|
||||
'Ctrl-L',
|
||||
],
|
||||
@@ -139,7 +163,13 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
['Ctrl-A', 'C'],
|
||||
['Ctrl-A', 'Ctrl-C'],
|
||||
'Ctrl-Shift-T',
|
||||
]
|
||||
],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'previous-word': ['Ctrl-ArrowLeft'],
|
||||
'next-word': ['Ctrl-ArrowRight'],
|
||||
'delete-previous-word': ['Ctrl-Backspace'],
|
||||
'delete-next-word': ['Ctrl-Delete'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -8,6 +8,34 @@ export class TerminalHotkeyProvider extends HotkeyProvider {
|
||||
id: 'copy',
|
||||
name: 'Copy to clipboard',
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
name: 'Paste from clipboard',
|
||||
},
|
||||
{
|
||||
id: 'home',
|
||||
name: 'Beginning of the line',
|
||||
},
|
||||
{
|
||||
id: 'end',
|
||||
name: 'End of the line',
|
||||
},
|
||||
{
|
||||
id: 'previous-word',
|
||||
name: 'Jump to previous word',
|
||||
},
|
||||
{
|
||||
id: 'next-word',
|
||||
name: 'Jump to next word',
|
||||
},
|
||||
{
|
||||
id: 'delete-previous-word',
|
||||
name: 'Delete previous word',
|
||||
},
|
||||
{
|
||||
id: 'delete-next-word',
|
||||
name: 'Delete next word',
|
||||
},
|
||||
{
|
||||
id: 'clear',
|
||||
name: 'Clear terminal',
|
||||
|
@@ -8,6 +8,15 @@ a:hover {
|
||||
|
||||
x-screen {
|
||||
transition: 0.125s ease background;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
x-row > span {
|
||||
|
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
@@ -41,6 +42,7 @@ import { hterm } from './hterm'
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgbModule,
|
||||
ToastrModule,
|
||||
],
|
||||
providers: [
|
||||
SessionsService,
|
||||
|
@@ -3,6 +3,8 @@ import { execFileSync } from 'child_process'
|
||||
import * as AsyncLock from 'async-lock'
|
||||
import { ConnectableObservable, AsyncSubject, Subject } from 'rxjs'
|
||||
import * as childProcess from 'child_process'
|
||||
|
||||
import { Logger } from 'terminus-core'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
declare function delay (ms: number): Promise<void>
|
||||
@@ -52,10 +54,11 @@ export class TMuxCommandProcess {
|
||||
private block$ = new Subject<TMuxBlock>()
|
||||
private response$: ConnectableObservable<TMuxBlock>
|
||||
private lock = new AsyncLock({ timeout: 1000 })
|
||||
private logger = new Logger(null, 'tmuxProcess')
|
||||
|
||||
constructor () {
|
||||
this.process = childProcess.spawn('tmux', ['-C', '-f', '/dev/null', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control'])
|
||||
console.log('[tmux] started')
|
||||
this.logger.log('started')
|
||||
this.process.stdout.on('data', data => {
|
||||
// console.debug('tmux says:', data.toString())
|
||||
this.rawOutput$.next(data.toString())
|
||||
@@ -103,18 +106,18 @@ export class TMuxCommandProcess {
|
||||
this.response$.connect()
|
||||
|
||||
this.block$.subscribe(block => {
|
||||
console.debug('[tmux] block:', block)
|
||||
this.logger.debug('block:', block)
|
||||
})
|
||||
|
||||
this.message$.subscribe(message => {
|
||||
console.debug('[tmux] message:', message)
|
||||
this.logger.debug('message:', message)
|
||||
})
|
||||
}
|
||||
|
||||
command (command: string): Promise<TMuxBlock> {
|
||||
return this.lock.acquire('key', () => {
|
||||
let p = this.response$.take(1).toPromise()
|
||||
console.debug('[tmux] command:', command)
|
||||
this.logger.debug('command:', command)
|
||||
this.process.stdin.write(command + '\n')
|
||||
return p
|
||||
}).then(response => {
|
||||
@@ -137,13 +140,18 @@ export class TMuxCommandProcess {
|
||||
export class TMux {
|
||||
private process: TMuxCommandProcess
|
||||
private ready: Promise<void>
|
||||
private logger = new Logger(null, 'tmux')
|
||||
|
||||
constructor () {
|
||||
this.process = new TMuxCommandProcess()
|
||||
this.ready = (async () => {
|
||||
for (let line of TMUX_CONFIG.split('\n')) {
|
||||
if (line) {
|
||||
await this.process.command(line)
|
||||
try {
|
||||
await this.process.command(line)
|
||||
} catch (e) {
|
||||
this.logger.warn('Skipping failing config line:', line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tmux sometimes sends a stray response block at start
|
||||
|
@@ -100,7 +100,7 @@ export class Session extends BaseSession {
|
||||
|
||||
this.open = true
|
||||
|
||||
this.pty.on('data', data => {
|
||||
this.pty.on('data-buffered', data => {
|
||||
this.emitOutput(data)
|
||||
})
|
||||
|
||||
@@ -200,7 +200,8 @@ export class SessionsService {
|
||||
electron: ElectronService,
|
||||
log: LogService,
|
||||
) {
|
||||
nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
|
||||
const nodePTYPath = electron.remoteResolvePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
|
||||
nodePTY = electron.remoteRequire('./bufferizedPTY')(nodePTYPath)
|
||||
this.logger = log.create('sessions')
|
||||
this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ export class TerminalService {
|
||||
|
||||
async openTab (shell?: IShell, cwd?: string): Promise<TerminalTabComponent> {
|
||||
if (!cwd) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
} else {
|
||||
cwd = this.config.store.terminal.workingDirectory || null
|
||||
|
@@ -36,12 +36,20 @@ export class WindowsStockShellsProvider extends ShellProvider {
|
||||
{ id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
|
||||
{
|
||||
id: 'powershell',
|
||||
name: 'PowerShell',
|
||||
name: 'Windows PowerShell',
|
||||
command: 'powershell.exe',
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'powershell-core',
|
||||
name: 'PowerShell Core',
|
||||
command: 'pwsh.exe',
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@ module.exports = {
|
||||
/^rxjs/,
|
||||
/^@angular/,
|
||||
/^@ng-bootstrap/,
|
||||
'ngx-toastr',
|
||||
/^terminus-/,
|
||||
],
|
||||
plugins: [
|
||||
|