Compare commits

...

48 Commits

Author SHA1 Message Date
Eugene Pankov
c3c983daf6 updated to the new NPM API 2018-03-31 13:23:32 +02:00
Eugene Pankov
dce8647f55 smart ctrl-c behaviour (fixes #307) 2018-03-30 23:42:50 +02:00
Eugene Pankov
f947fe3f0f paste as a configurable hotkey (fixes #260) 2018-03-30 23:33:46 +02:00
Eugene Pankov
b5f96a59f8 copy notification 2018-03-30 23:24:34 +02:00
Eugene Pankov
c90a5678cf don't repatch node-pty on window reload 2018-03-29 14:23:33 +02:00
Eugene Pankov
663da34e6d performance improv for flowing output 2018-03-29 00:25:57 +02:00
Eugene Pankov
049f08b8f9 namespacing fix 2018-03-24 23:45:40 +01:00
Eugene Pankov
3c3b14bf09 Smarted spawn hotkey behaviour on macOS to give the focus to the previous app on hide 2018-03-24 23:40:45 +01:00
Eugene Pankov
5e07dd5442 macOS touchbar support 2018-03-24 23:19:47 +01:00
Eugene Pankov
8f2d2cbe30 don't attempt to resize beyond min size when docking (fixes #308) 2018-03-23 17:53:20 +01:00
Eugene Pankov
bebde4799d updated tab design 2018-03-23 17:45:11 +01:00
Eugene Pankov
9cedeb3efb build fixes 2018-03-23 17:15:11 +01:00
Eugene Pankov
63158ac6cd package.json fix 2018-03-18 19:07:31 +01:00
Eugene Pankov
4f44087989 possibly npm $PATH (fixes #305, fixes #4) 2018-03-18 18:59:58 +01:00
Eugene Pankov
ab3c49b9b2 upgraded electron (fixes #306) 2018-03-18 17:36:19 +01:00
Eugene Pankov
28d01a1b56 added gnome-python2-gnomekeyring RPM dependency (fixes #302) 2018-03-13 11:37:59 +01:00
Eugene Pankov
a979f0108e added a tray icon (fixes #226) 2018-03-11 20:29:53 +01:00
Eugene Pankov
3c74b8ec38 possible fix to docking (fixes #9, fixes #18) 2018-03-11 20:07:02 +01:00
Eugene Pankov
9d7bf2ae44 skip failing tmux init commands (fixes #300) 2018-03-11 20:01:48 +01:00
Eugene Pankov
3b43b3914b added powershell core as a separate shell (#123) 2018-03-11 19:53:13 +01:00
Eugene Pankov
e9f22dd8b5 yarn bump 2018-03-11 19:53:10 +01:00
Eugene Pankov
e68cafdb70 fullscreen mode (fixes #283) 2018-02-12 17:04:49 +01:00
Eugene Pankov
fde16b8699 fixed #291 2018-02-12 16:54:04 +01:00
Eugene Pankov
245c65d750 bumped electron 2018-02-12 16:52:40 +01:00
Eugene Pankov
c7d9f944d5 Merge pull request #285 from VaultVulp/master
Fixed typo in SSH client's configuration window.
2018-02-03 10:11:17 +01:00
Pavel Alimpiev
4ca806e142 Fixed typo in SSH client's configuration window. 2018-02-03 01:15:55 +03:00
Eugene Pankov
0255985bc6 icon update 2018-01-24 19:39:51 +01:00
Eugene Pankov
104f1ee7aa yarn.lock 2018-01-24 16:40:35 +01:00
Eugene Pankov
132d0553ae fixed alt-arrow keys on Mac as well as Home and End combinations (fixes #255) 2018-01-24 16:40:30 +01:00
Eugene Pankov
b007ff6ff6 scrollbar fix 2018-01-24 16:01:32 +01:00
Eugene Pankov
2bea4b9d6c bump 2018-01-19 15:57:33 +01:00
Eugene Pankov
4a76c12f15 ignore Hyper theme errors 2018-01-19 15:53:14 +01:00
Eugene Pankov
181f3e3d33 Merge branch 'master' of github.com:Eugeny/terminus 2018-01-19 15:37:51 +01:00
Eugene Pankov
ee2fadbf60 added message popups 2018-01-19 15:31:28 +01:00
Eugene Pankov
4259d3b53d enforce GPU composition 2018-01-19 15:30:52 +01:00
Eugene Pankov
65aaa131ef removed Screen hotkeys from the standard macOS config 2018-01-04 21:38:17 +01:00
Eugene Pankov
46d9aabbdd configure() doesn't have to be async 2018-01-04 21:38:02 +01:00
Eugene Pankov
692045ce77 fixed button alignment on macOS 2018-01-04 21:20:42 +01:00
Eugene Pankov
9c257b0002 allow specifying the SSH port (fixes #269) 2018-01-04 21:13:46 +01:00
Eugene Pankov
15c23eb7dd deps 2018-01-04 21:04:44 +01:00
Eugene Pankov
5fc67d3648 launch devtools in detached mode 2018-01-04 21:04:31 +01:00
Eugene Pankov
571884f39c Merge branch 'master' of github.com:Eugeny/terminus 2017-12-27 23:12:50 +01:00
Eugene Pankov
ccbcd30813 support encrypted private ssh keys (fixes #262) 2017-12-27 23:11:28 +01:00
Eugene Pankov
30666c2838 Merge pull request #241 from tnguyen14/base16-xresources
use base16-xresources
2017-12-15 19:07:55 +01:00
Tri Nguyen
953558a866 process #define variables 2017-12-14 23:51:21 -05:00
Eugene Pankov
ace81aced2 Merge pull request #261 from kwonoj/feat-tab-cycle
feat(tab): enable cycle tab selection
2017-12-14 19:48:48 +01:00
OJ Kwon
dc781deeb0 feat(tab): enable cycle tab selection 2017-12-14 08:40:00 -08:00
Tri Nguyen
e3d1d5e61e use base16-xresources 2017-11-27 23:37:07 -05:00
78 changed files with 1567 additions and 1012 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

BIN
app/assets/tray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

50
app/bufferizedPTY.js Normal file
View 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
}

View File

@@ -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'},

View File

@@ -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"

View File

@@ -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)),

View File

@@ -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'

View File

@@ -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 = ''

View File

@@ -59,6 +59,7 @@ const builtinModules = [
'@angular/platform-browser',
'@angular/platform-browser-dynamic',
'@ng-bootstrap/ng-bootstrap',
'ngx-toastr',
'rxjs',
'terminus-core',
'terminus-settings',

View File

@@ -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
View 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;
}
}
}

View File

@@ -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',

View File

@@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

View File

@@ -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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -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"
}

View File

@@ -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('..')
}

View File

@@ -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

View File

@@ -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

View File

@@ -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[] = []

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -14,8 +14,6 @@ $tabs-height: 36px;
transition: 0.125s ease-out all;
border-top: 1px solid transparent;
.index {
flex: none;
font-weight: bold;

View File

@@ -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)
}

View File

@@ -1,6 +1,8 @@
hotkeys:
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen:
- 'F11'
close-tab:
- 'Ctrl-Shift-W'
- ['Ctrl-A', 'K']

View File

@@ -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']

View File

@@ -1,6 +1,8 @@
hotkeys:
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen:
- 'F11'
close-tab:
- 'Ctrl-Shift-W'
- ['Ctrl-A', 'K']

View File

@@ -3,6 +3,7 @@ appearance:
dockScreen: current
dockFill: 50
tabsLocation: top
cycleTabs: true
theme: Standard
frame: thin
css: '/* * { color: blue !important; } */'

View File

@@ -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 },

View File

@@ -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 () {

View File

@@ -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

View File

@@ -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:')
}
}
}

View File

@@ -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')
}

View File

@@ -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',

View File

@@ -5,7 +5,7 @@ export const metaKeyName = {
}[process.platform]
export const altKeyName = {
darwin: 'Option',
darwin: '',
win32: 'Alt',
linux: 'Alt',
}[process.platform]

View File

@@ -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) }

View 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)
}
}

View File

@@ -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%);
}
}
}

View File

@@ -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),

View File

@@ -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)

View File

@@ -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",

View File

@@ -3,6 +3,7 @@ import { BaseSession } from 'terminus-terminal'
export interface SSHConnection {
name?: string
host: string
port: number
user: string
password?: string
privateKey?: string

View File

@@ -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()
}

View File

@@ -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(

View File

@@ -3,7 +3,7 @@
type='text',
[(ngModel)]='quickTarget',
autofocus,
placeholder='Quick connect: [user@]host',
placeholder='Quick connect: [user@]host[:port]',
(keyup.enter)='quickConnect()'
)

View File

@@ -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}`)
})
}

View File

@@ -21,6 +21,7 @@ export class SSHSettingsTabComponent {
let connection: SSHConnection = {
name: '',
host: '',
port: 22,
user: 'root',
}
let modal = this.ngbModal.open(EditConnectionModalComponent)

View File

@@ -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 },

View 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)
}
}
})
}
}

View File

@@ -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
}

View File

@@ -40,6 +40,7 @@ module.exports = {
'xkeychain',
'wincredmgr',
'path',
'ngx-toastr',
/^rxjs/,
/^@angular/,
/^@ng-bootstrap/,

View File

@@ -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"

View File

@@ -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,

View File

@@ -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)

View File

@@ -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'],
},
},
}

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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',
}
},
]
}
}

View File

@@ -55,6 +55,7 @@ module.exports = {
/^rxjs/,
/^@angular/,
/^@ng-bootstrap/,
'ngx-toastr',
/^terminus-/,
],
plugins: [

1365
yarn.lock

File diff suppressed because it is too large Load Diff