Compare commits

..

106 Commits

Author SHA1 Message Date
Eugene Pankov
329d0448d3 reconfigure terminals on DPI change (fixes #576) 2019-02-10 00:23:49 +01:00
Eugene Pankov
100436f511 set xterm as default frontend (fixes #542) 2019-02-09 22:40:31 +01:00
Eugene Pankov
22d3e35723 ignore events on destroyed windows 2019-02-09 22:38:45 +01:00
Eugene Pankov
9cdcc8d8e5 fixed #649 2019-02-09 22:10:42 +01:00
Eugene Pankov
168e6f17dc allow selecting ssh ciphers (fixes #645) 2019-02-09 18:52:09 +01:00
Eugene Pankov
a2c636fdbf console logging 2019-02-09 17:44:23 +01:00
Eugene Pankov
413ca70729 Warn when enabling ConPTY on older insider builds (fixes #609, fixes #594) 2019-02-09 17:44:17 +01:00
Eugene Pankov
6f99e6c14b Merge branch 'master' of github.com:Eugeny/terminus 2019-01-30 19:46:25 +01:00
Eugene Pankov
e65811786d bumped node-ssh2 (fixes #605) 2019-01-30 18:11:49 +01:00
Eugene
aecd381b25 Merge pull request #633 from sylveon/master
Update windows-swca dependency
2019-01-30 16:48:11 +01:00
Eugene Pankov
89465f57d5 bumped node-pty 2019-01-30 13:02:35 +01:00
Charles Milette
3bf0ac43ef Update yarn lockfile 2019-01-29 15:37:40 -05:00
Charles Milette
a66dd43e1e Update windows-swca dependency 2019-01-29 15:33:50 -05:00
Eugene Pankov
dd4566cf02 #618 fixes 2019-01-27 23:58:55 +01:00
Eugene Pankov
f2be34d137 limit max font size (fixes #618) 2019-01-27 23:40:33 +01:00
Eugene Pankov
e28c619bdc force en-us locale (#618) 2019-01-27 23:39:05 +01:00
Eugene Pankov
04bf5dbcfb fixed offset with tabs on bottom on macos (fixes #629) 2019-01-27 23:12:46 +01:00
Eugene Pankov
a2128ca1f2 use ssh connection name for the tab's title (fixes #621) 2019-01-27 22:56:50 +01:00
Eugene Pankov
bf0d02d1fc tab duplication (fixes #588) 2019-01-27 22:45:08 +01:00
Eugene Pankov
792de65696 properly recover tabs with xterm 2019-01-27 22:01:55 +01:00
Eugene Pankov
fab21f6859 mention save-output plugin 2019-01-27 22:01:46 +01:00
Eugene
b0b01b98be Update README.md 2019-01-21 15:52:55 +01:00
Eugene
24dff4b5b7 Update README.md 2019-01-18 13:56:08 +00:00
Eugene Pankov
78f8f4005e fixed #610 2019-01-16 17:13:34 +00:00
Eugene Pankov
38cfb3f036 middle click to paste (fixes #613) 2019-01-16 16:46:01 +00:00
Eugene Pankov
4e4d8a0e91 bumped node-pty 2019-01-16 16:25:43 +00:00
Eugene Pankov
21cfd14f1c use the upstream xtermjs 2019-01-16 16:16:06 +00:00
Eugene Pankov
a64bbe145c fixed automatic resizing with xterm 2019-01-16 15:23:55 +00:00
Eugene Pankov
6a5dc79c5d bumped plugin versions 2019-01-10 12:44:12 +01:00
Eugene Pankov
b799128427 fixed TerminalContextMenuProvider typing 2019-01-10 12:44:07 +01:00
Eugene Pankov
8b64a819e7 expose DOM element ref from BaseTerminalTab 2019-01-08 16:37:54 +03:00
Eugene Pankov
5b78a5c1ed made tab context menu extensible 2019-01-07 19:30:03 +03:00
Eugene Pankov
91b318853f replace the stock installer gif (fixes #606) 2019-01-07 17:31:16 +03:00
Eugene Pankov
ce3610c2da automatically recover ssh tabs (fixes #583) 2019-01-06 11:54:26 +01:00
Eugene Pankov
d03430fb2e ssh - show connection log while connecting 2019-01-06 11:14:13 +01:00
Eugene Pankov
caacc01aea split common terminal behaviour into BaseTerminalTab 2019-01-05 16:54:22 +01:00
Eugene Pankov
bcb6963c35 show ssh connection errors 2019-01-05 15:19:02 +01:00
Eugene Pankov
deb99b0865 wrap TerminalTab into SSHTab 2019-01-05 15:17:41 +01:00
Eugene Pankov
2101c18657 fixed saving ssh connections (fixes #436) 2019-01-05 15:03:31 +01:00
Eugene Pankov
1a258f32b0 fixed npm detection when fish is the default shell (#584) 2019-01-05 14:53:19 +01:00
Eugene Pankov
3aaf490f57 fixed #597 2019-01-05 14:51:36 +01:00
Eugene Pankov
9faa346699 better messageboxes 2019-01-03 17:20:02 +03:00
Eugene Pankov
d5b6a686f8 added settings tab icons 2019-01-03 17:19:50 +03:00
Eugene Pankov
492d006f64 xterm scrollback fix 2019-01-03 17:07:38 +03:00
Eugene Pankov
d999320c24 bumped plugin versions 2019-01-03 13:08:57 +03:00
Eugene Pankov
5142d12e7e fixed macos zip artifact naming 2019-01-03 13:01:15 +03:00
Eugene Pankov
453c613571 bumped xterm scrollback size (fixes #589) 2019-01-03 12:55:14 +03:00
Eugene Pankov
ccc34ae4d9 bumped angular to rc 2018-12-30 17:53:07 +01:00
Eugene Pankov
4362b5c50b bumped node-gyp 2018-12-30 17:44:52 +01:00
Eugene Pankov
2d6023446c bumped electron-builder 2018-12-30 17:35:09 +01:00
Eugene Pankov
dcd43dc019 fixed the Preferences menu item 2018-12-30 17:32:30 +01:00
Eugene Pankov
d8e70f9693 bumped nodejs on travis 2018-12-30 17:26:01 +01:00
Eugene Pankov
7a26e8bd65 ignore non-existent CWDs (fixes #586) 2018-12-30 15:59:40 +01:00
Eugene Pankov
d56287587c bumped electron to stable 2018-12-30 15:54:17 +01:00
Eugene Pankov
8793613117 potentially fixed #576 2018-12-29 13:27:45 +01:00
Eugene Pankov
92afec75e7 fixed plugin blacklisting 2018-12-29 12:50:14 +01:00
Eugene Pankov
ca71ec24f8 fixed #585 2018-12-29 12:41:32 +01:00
Eugene Pankov
524550f6e3 made context menu extensible 2018-12-24 19:41:27 +01:00
Eugene Pankov
fe31131fc1 typo fix 2018-12-24 18:40:29 +01:00
Eugene Pankov
a7c1fe3425 Experimental UAC start-as-admin wrapper (fixes #511) 2018-12-24 18:11:26 +01:00
Eugene Pankov
d7b305bf29 fixes in profile editor 2018-12-24 17:22:27 +01:00
Eugene Pankov
0bd0c850da fixed profile duplication 2018-12-24 11:32:04 +01:00
Eugene Pankov
88bb40f94b offer shell selection in the terminal context menu 2018-12-23 21:03:09 +01:00
Eugene Pankov
120e2a2cd5 fixed --login flag for older shells 2018-12-23 21:02:18 +01:00
Eugene Pankov
cbb6821814 don't set an empty jumplist 2018-12-23 20:56:39 +01:00
Eugene Pankov
75bf374a8f build fix 2018-12-23 20:56:30 +01:00
Eugene Pankov
bf995981d3 use yarn on appveyor & autoinstall plugin deps 2018-12-23 20:03:29 +01:00
Eugene Pankov
a6fdabcd2f removed debug logging 2018-12-22 11:15:50 +01:00
Eugene Pankov
0e6886d00a fixed args field focus 2018-12-22 11:15:40 +01:00
Eugene Pankov
459d6aadd9 fixed beam cursor on xterm (fixes #582) 2018-12-22 09:39:17 +01:00
Eugene Pankov
21d533c7cf attempt to detect CWD on classic windows shells 2018-12-22 01:36:05 +01:00
Eugene Pankov
211566488d removed default ctrl-a hotkeys (fixes #578) 2018-12-21 23:49:30 +01:00
Eugene Pankov
282aab2e55 fixed alt-v passing in hterm (fixes #560) 2018-12-21 23:48:44 +01:00
Eugene Pankov
6f41865474 lint 2018-12-21 23:18:22 +01:00
Eugene Pankov
e4bcfd8f39 bumped node-pty 2018-12-21 23:11:53 +01:00
Eugene Pankov
504cfcf8ff build fix (fixes 579) 2018-12-21 23:06:24 +01:00
Eugene Pankov
6e13914712 fixed nightly builds (fixes #579) 2018-12-21 21:52:12 +01:00
Eugene Pankov
9aaf670092 fontawesome 5 2018-12-21 21:37:34 +01:00
Eugene Pankov
c204f6d5a4 use providedIn 2018-12-21 21:21:33 +01:00
Eugene Pankov
91bba042b5 made conpty optional 2018-12-21 21:05:59 +01:00
Eugene Pankov
2ca6135c06 ui tweaks 2018-12-21 20:43:11 +01:00
Eugene Pankov
9ef3cbc177 profile args editor 2018-12-21 20:06:03 +01:00
Eugene Pankov
8a3906687a Merge branch 'master' into persistence 2018-12-21 20:04:49 +01:00
Eugene
3192a14c9d Merge pull request #568 from ehwarren/feature/rename-tab-qol
Feature/rename tab qol
2018-12-19 10:17:08 +01:00
Austin Warren
b510a86f4d Change rename hotkey to ⌘-R 2018-12-18 17:20:08 -08:00
Austin Warren
fcf14eaa8b Move focus to RenameTabModalComponent onInit 2018-12-18 17:19:41 -08:00
Eugene Pankov
137dd0bbe8 profile editor, env vars editor, creating profiles from shell list 2018-12-18 15:08:23 +01:00
Eugene Pankov
4b5b75a57a ui 2018-12-17 20:41:08 +01:00
Eugene Pankov
68c497e5fc windows jumplist integration 2018-12-16 23:20:35 +01:00
Eugene Pankov
1da7c85973 cli option to launch a specific profile 2018-12-16 23:13:14 +01:00
Eugene Pankov
fe75aab724 show profiles in macos dock item menu 2018-12-16 23:02:17 +01:00
Eugene Pankov
85bcac1fb7 profile settings 2018-12-16 17:41:30 +01:00
Eugene Pankov
72287cc7cb profile settings tab 2018-12-16 17:09:35 +01:00
Eugene Pankov
1f1d212c1d build fix 2018-12-16 15:57:08 +01:00
Eugene Pankov
cded1284de simpler tab recovery system 2018-12-16 15:42:04 +01:00
Eugene Pankov
df97e7ebb5 updated wsl truecolor warning 2018-12-15 23:51:03 +01:00
Eugene Pankov
d80c9a27d3 pulled in the freshest node-pty (fixes #23) 2018-12-15 23:44:20 +01:00
Eugene Pankov
3469ec9b6b fixed blur (fixes #556) 2018-12-15 15:59:16 +01:00
Eugene Pankov
d4db8f4b18 build script fixes 2018-12-15 15:49:06 +01:00
Eugene Pankov
384744ec44 only enable agent forwarding on windows when pageant is running (fixes #496) 2018-12-15 15:19:47 +01:00
Eugene Pankov
76633db25e migrate back to stock rage-edit 2018-12-15 14:27:12 +01:00
Austin Warren
6b823d0fa0 Fixed build errors 2018-12-14 14:50:38 -08:00
Austin Warren
798dda5236 Added rename-tab hotkey 2018-12-14 14:50:16 -08:00
Austin Warren
2b90a17d5e Added hotkey rename, right click rename, and auto select text in rename modal 2018-12-14 14:49:20 -08:00
Eugene Pankov
6387539980 bumped angular 2018-12-13 18:01:44 +01:00
Eugene Pankov
cb17fd0866 fixed #564 2018-12-13 18:00:59 +01:00
142 changed files with 2758 additions and 2183 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules
build/files.wxs build/files.wxs
dist dist
*/dist
*.xcworkspacedata *.xcworkspacedata
*.xcuserstate *.xcuserstate

View File

@@ -6,7 +6,7 @@ matrix:
env: BUILD_FOR=macos env: BUILD_FOR=macos
language: node_js language: node_js
node_js: 8 node_js: 10
cache: cache:
directories: directories:
@@ -14,8 +14,7 @@ cache:
- app/node_modules - app/node_modules
before_install: before_install:
- yarn install - yarn
- scripts/install-deps.js
script: script:
- scripts/build-native.js - scripts/build-native.js

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ export function parseArgs (argv, cwd) {
.command('run [command...]', 'run a command in the terminal', { .command('run [command...]', 'run a command in the terminal', {
command: { type: 'string' }, command: { type: 'string' },
}) })
.command('profile [profileName]', 'open a tab with specified profile', {
profileName: { type: 'string' },
})
.command('paste [text]', 'paste stdin into the active tab', yargs => { .command('paste [text]', 'paste stdin into the active tab', yargs => {
return yargs.option('escape', { return yargs.option('escape', {
alias: 'e', alias: 'e',

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import '../lib/lru' import '../lib/lru'
import 'source-sans-pro' import 'source-sans-pro'
import 'source-code-pro/source-code-pro.css' import 'source-code-pro/source-code-pro.css'
import 'font-awesome/css/font-awesome.css' import '@fortawesome/fontawesome-free/css/solid.css'
import '@fortawesome/fontawesome-free/css/brands.css'
import '@fortawesome/fontawesome-free/css/fontawesome.css'
import 'ngx-toastr/toastr.css' import 'ngx-toastr/toastr.css'
import './preload.scss' import './preload.scss'

View File

@@ -15,6 +15,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { getRootModule } from './app.module' import { getRootModule } from './app.module'
import { findPlugins, loadPlugins, IPluginInfo } from './plugins' import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
;(process as any).enablePromiseAPI = true
if (process.platform === 'win32') { if (process.platform === 'win32') {
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
} }
@@ -34,7 +36,7 @@ async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgM
}) })
let module = getRootModule(pluginsModules) let module = getRootModule(pluginsModules)
window['rootModule'] = module window['rootModule'] = module
return await platformBrowserDynamic().bootstrapModule(module) return platformBrowserDynamic().bootstrapModule(module)
} }
findPlugins().then(async plugins => { findPlugins().then(async plugins => {

View File

@@ -16,11 +16,11 @@ function normalizePath (path: string): string {
nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x))) nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x)))
if (process.env.DEV) { if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath())) nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
} }
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins') const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const userPluginsPath = path.join( const userPluginsPath = path.join(
require('electron').remote.app.getPath('appData'), require('electron').remote.app.getPath('appData'),
@@ -118,7 +118,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
} }
try { try {
let info = JSON.parse(await fs.readFile(infoPath, {encoding: 'utf-8'})) let info = JSON.parse(await fs.readFile(infoPath, { encoding: 'utf-8' }))
if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) { if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
continue continue
} }

View File

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

View File

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

View File

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

View File

@@ -2,52 +2,52 @@
# yarn lockfile v1 # yarn lockfile v1
"@angular/animations@7.0.0": "@angular/animations@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.0.0.tgz#5c9e1683063c29df10253b7dc5bb9b13694ee396" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-rc.0.tgz#12849f8ab104d309ec99c0ceb170a895c15d3d44"
integrity sha512-IYdryQXdYfPvhJpExLSAr0o9rlUeyVS++a6h/sjqN1dkUt/yJBHLRreuHx8Udvlj2nH70raHJgevk8FwhAkTdA== integrity sha512-CRQNQ6QVTuf4nCHVLVpKQx7YPpNPfnTF79KVWzHefkkyS3URRuEgvE4jCED4oTJ4BEsmkjXyt51VeDV0FgqQFg==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/common@7.0.0": "@angular/common@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.0.0.tgz#29206614d2b8dc79e5207b7dc6f9fc559e9a24f2" resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-rc.0.tgz#60d3540c6cdcf3440f67e2c15cf8f1c7b1160d9d"
integrity sha512-jp6MA6EOq/a1m+F0c1aZC345pAYYYFpN1m7GMM91JlqkjzJMhyYVk+Bod9xQOEWadcpY+RFudG+jRsPCMO8bvQ== integrity sha512-Xv60KEP1kpF74kpN1xtps0W++PUXLUMK/0tDblUZH7tBWvS0XwEwtuK5B6wcs+I5nqZkPgvlvOyiVZvOLraWOg==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/compiler@7.0.0": "@angular/compiler@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.0.0.tgz#f953a213a01e4736e94fe1a370b07e13e2393b71" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-rc.0.tgz#603dbec25d6c2beea08a293c68c39b40e2ea81e2"
integrity sha512-4fkohfGyG1BEpeYenOartuJmduyZ/R3XQx46hDDiR/9A8/Go4qLGkgr9Bd/JL/gPIR1XAHH9D5ii2sh+28ZEmA== integrity sha512-tvgGJx0urSz/qn6upmcjX3N3dyWQ9m5mQOwJxmN4qekxjOtSRml5yt2KtlaUTkGsjkEmEVfSHel+X1TwzBdhYw==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/core@7.0.0": "@angular/core@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.0.0.tgz#01e9db9074a1db1c47a32f745b787d1c86f5d61a" resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-rc.0.tgz#57c0e26130288c3b58466f079828c028bdf6221f"
integrity sha512-DjVyWNGBWKEeBvxeXy8FGBNlnr/W/tNygOZEd6/uCktcXTG4DNyNQrWuNZUKEpr7RuIT3YVMj+UNwgTq0jB/9g== integrity sha512-2u11TNlLorw3JhuczCPwl8UmxE+ja2Q/ghBl8iYi4SIBWiBO1K0wVT13Ts7eojk63yZcg60lyYYCegXBmHLTuw==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/forms@7.0.0": "@angular/forms@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.0.0.tgz#672c306b13e94a20b72c096214642a326c43699a" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-rc.0.tgz#4eb473018084bb81be3e2e1ae8afa8d2b2117a6c"
integrity sha512-rTg1UHq9gHR6zY3Kkip1KCm/YTck/rlR8CvVFIMwF0bdQxUCT51SXVn58nXts9yDaieABcGaQHNkQn1mARslgw== integrity sha512-OWP1zzYQiuqtoltdlhkcVjHxg78exbt7z1lr8RSjybr/Snc5zSFhnZF6byasd/4lzVySuujsMXkTK7D8x6hedA==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/platform-browser-dynamic@7.0.0": "@angular/platform-browser-dynamic@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.0.0.tgz#2b2a50b5a8176bee257f90ee47b1d873502f7182" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-rc.0.tgz#5ea47d094c53a0ba34ecbb0dfdcef452fa05dc9a"
integrity sha512-lH2KuH+Em1y/mTOE6yTJmsOxYkMbYKzKLP9gYzc9vZu3er1df6Jx6jxefeBmAr9v+kNCLnpnHWHz2y4GzAesJA== integrity sha512-uqT27oh9m58L6MUjgvT+7NpAFbigOnnTUWMsCLijNUKd7i37T6UxTVKPvuqNHlaLXsmDRxVHN3INI0IrWZ3R+w==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/platform-browser@7.0.0": "@angular/platform-browser@7.2.0-rc.0":
version "7.0.0" version "7.2.0-rc.0"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.0.0.tgz#8c13a6380cf465b3628e5b576a1313e9b4976093" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-rc.0.tgz#c6d1f0b2328b1d81649bea70c23edc33de729015"
integrity sha512-XyvL30d6meJ+SXlOmdR+sxoLdSvkQdmVNvpdvUzAHC/EqwA/byg4V3bTe5lpZmypclgFCjkGoTsz6uOnnwlQhw== integrity sha512-r0ak7SVLWrivd4S0MXWmqNLeF6NNOBAopnjrhUu2j5I00u7/QfLrX0E5zRlJ8JkARVjer6Wm+D1ztlOWw5jHag==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
@@ -70,6 +70,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
integrity sha512-Y3EAG7VA7NVNbZek/fjJtILnmTk/ZfpJuWZGDBqDZ1dVIxgJJJ82fXPW7pKnqyV9CD/9bcPOCi7eErUqGMHOrA== integrity sha512-Y3EAG7VA7NVNbZek/fjJtILnmTk/ZfpJuWZGDBqDZ1dVIxgJJJ82fXPW7pKnqyV9CD/9bcPOCi7eErUqGMHOrA==
"@types/node@^10.12.18":
version "10.12.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
accessibility-developer-tools@^2.11.0: accessibility-developer-tools@^2.11.0:
version "2.12.0" version "2.12.0"
resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514" resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
@@ -620,10 +625,12 @@ windows-blurbehind@^1.0.0:
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.0.tgz#050efb988704c44335bdc3efefd757f6e463b8ac" resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.0.tgz#050efb988704c44335bdc3efefd757f6e463b8ac"
integrity sha512-lO+A7fhTHO7oy9zJM3o1AdzfSQrmtPkdwvleeuww840ghijjEA1f1Zp8bKA3mJu2DFNtVT40fwmqtgsCGat4UA== integrity sha512-lO+A7fhTHO7oy9zJM3o1AdzfSQrmtPkdwvleeuww840ghijjEA1f1Zp8bKA3mJu2DFNtVT40fwmqtgsCGat4UA==
windows-swca@^1.1.1: windows-swca@^2.0.1:
version "1.1.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-1.1.1.tgz#0b3530278c67d408baac71c3a6aeb16d55318bf8" resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-2.0.1.tgz#25d78ce25251292061494a0ad07c02282b28b4e3"
integrity sha512-hKmHrNYJD72Kg0u35fjkiFIuMKuC+Tztmf3Obnf4aTkNjstEpbSEspEeSo3ZNixaVCETA1dLbDkVUQVF1QxtWA== integrity sha512-flj+HD6RUemZUvKKguLLnMUOYkQSgDu9qrhUIO4cydvtb/x+sxU8XmpZUtugYuydcdikB9zsCOMgKnAqIQ+7nw==
dependencies:
"@types/node" "^10.12.18"
wrap-ansi@^2.0.0: wrap-ansi@^2.0.0:
version "2.1.0" version "2.1.0"

View File

@@ -7,18 +7,18 @@ environment:
nodejs_version: "10" nodejs_version: "10"
cache: cache:
- '%USERPROFILE%\.electron' - "%USERPROFILE%\\.electron"
- "%LOCALAPPDATA%\\Yarn"
version: "{build}" version: "{build}"
install: install:
- ps: Install-Product node $env:nodejs_version $env:platform - ps: Install-Product node $env:nodejs_version $env:platform
- npm install - yarn
- node scripts/install-deps.js
- node scripts/build-native.js - node scripts/build-native.js
build_script: build_script:
- npm run build - yarn run build
- node scripts/prepackage-plugins.js - node scripts/prepackage-plugins.js
- node scripts/build-windows.js - node scripts/build-windows.js

BIN
build/windows/squirrel.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
extras/UAC.exe Normal file

Binary file not shown.

View File

@@ -1,6 +1,7 @@
{ {
"name": "term", "name": "term",
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^5.6.3",
"@types/electron-config": "^0.2.1", "@types/electron-config": "^0.2.1",
"@types/electron-debug": "^1.1.0", "@types/electron-debug": "^1.1.0",
"@types/fs-promise": "1.0.1", "@types/fs-promise": "1.0.1",
@@ -13,20 +14,17 @@
"core-js": "2.4.1", "core-js": "2.4.1",
"cross-env": "4.0.0", "cross-env": "4.0.0",
"css-loader": "0.28.0", "css-loader": "0.28.0",
"electron": "4.0.0-beta.8", "electron": "4.0.0",
"electron-builder": "^20.38.2", "electron-builder": "^20.38.4",
"electron-builder-squirrel-windows": "^20.28.3", "electron-builder-squirrel-windows": "^20.28.3",
"electron-installer-snap": "^3.0.0", "electron-installer-snap": "^3.0.0",
"electron-rebuild": "^1.8.2", "electron-rebuild": "^1.8.2",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"font-awesome": "4.7.0",
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",
"html-loader": "0.4.4", "html-loader": "0.4.4",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"less": "2.7.1",
"less-loader": "2.2.3",
"node-abi": "^2.4.4", "node-abi": "^2.4.4",
"node-gyp": "^3.6.2", "node-gyp": "^3.8.0",
"node-sass": "^4.5.3", "node-sass": "^4.5.3",
"npmlog": "4.1.0", "npmlog": "4.1.0",
"npx": "^10.2.0", "npx": "^10.2.0",
@@ -44,17 +42,20 @@
"style-loader": "0.13.1", "style-loader": "0.13.1",
"svg-inline-loader": "^0.8.0", "svg-inline-loader": "^0.8.0",
"to-string-loader": "1.1.5", "to-string-loader": "1.1.5",
"tslint": "5.1.0", "tslint": "^5.12.0",
"tslint-config-standard": "5.0.2", "tslint-config-standard": "^8.0.1",
"tslint-eslint-rules": "4.0.0", "tslint-eslint-rules": "^5.4.0",
"typescript": "^3.1.3", "typescript": "^3.1.3",
"url-loader": "^1.1.1", "url-loader": "^1.1.1",
"val-loader": "0.5.0", "val-loader": "0.5.0",
"webpack": "^4.22.0", "webpack": "^4.27.1",
"webpack-cli": "^3.1.2", "webpack-cli": "^3.1.2",
"yaml-loader": "0.4.0", "yaml-loader": "0.4.0",
"yarn": "^1.10.1" "yarn": "^1.10.1"
}, },
"resolutions": {
"*/node-abi": "^2.5.0"
},
"build": { "build": {
"appId": "org.terminus", "appId": "org.terminus",
"productName": "Terminus", "productName": "Terminus",
@@ -76,6 +77,7 @@
}, },
"squirrelWindows": { "squirrelWindows": {
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico", "iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
"loadingGif": "./build/windows/squirrel.gif",
"artifactName": "terminus-${version}-setup.exe" "artifactName": "terminus-${version}-setup.exe"
}, },
"portable": { "portable": {
@@ -84,6 +86,7 @@
"mac": { "mac": {
"category": "public.app-category.video", "category": "public.app-category.video",
"icon": "./build/mac/icon.icns", "icon": "./build/mac/icon.icns",
"artifactName": "terminus-${version}-macos.${ext}",
"publish": [ "publish": [
"github" "github"
], ],
@@ -124,11 +127,11 @@
}, },
"scripts": { "scripts": {
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js", "build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
"watch": "cross-env DEV=1 webpack --progress --color --watch", "watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
"start": "cross-env DEV=1 electron app --debug", "start": "cross-env TERMINUS_DEV=1 electron app --debug",
"prod": "cross-env DEV=1 electron app", "prod": "cross-env TERMINUS_DEV=1 electron app",
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts", "lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
"postinstall": "install-app-deps" "postinstall": "node ./scripts/install-deps.js"
}, },
"repository": "eugeny/terminus" "repository": "eugeny/terminus"
} }

View File

@@ -4,24 +4,18 @@ const path = require('path')
const vars = require('./vars') const vars = require('./vars')
lifecycles = [] lifecycles = []
lifecycles.push(rebuild({ for (let dir of ['app', 'terminus-ssh', 'terminus-terminal']) {
buildPath: path.resolve(__dirname, '../app'), lifecycles.push([rebuild({
electronVersion: vars.electronVersion, buildPath: path.resolve(__dirname, '../' + dir),
force: true, electronVersion: vars.electronVersion,
}).lifecycle) force: true,
lifecycles.push(rebuild({ }).lifecycle, dir])
buildPath: path.resolve(__dirname, '../terminus-ssh'), }
electronVersion: vars.electronVersion,
force: true,
}).lifecycle)
lifecycles.push(rebuild({
buildPath: path.resolve(__dirname, '../terminus-terminal'),
electronVersion: vars.electronVersion,
force: true,
}).lifecycle)
for (let lc of lifecycles) { console.info('Building against Electron', vars.electronVersion)
for (let [lc, dir] of lifecycles) {
lc.on('module-found', name => { lc.on('module-found', name => {
console.info('Rebuilding', name) console.info('Rebuilding', dir + '/' + name)
}) })
} }

View File

@@ -8,7 +8,6 @@ const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
const npx = `${localBinPath}/npx`; const npx = `${localBinPath}/npx`;
log.info('deps', 'app') log.info('deps', 'app')
sh.exec(`${npx} yarn install`)
sh.cd('app') sh.cd('app')
sh.exec(`${npx} yarn install`) sh.exec(`${npx} yarn install`)

View File

@@ -3,10 +3,11 @@ const fs = require('fs')
const childProcess = require('child_process') const childProcess = require('child_process')
const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json'))) const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json')))
const pkgInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'))) const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'}) exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
exports.version = exports.version.substring(1, exports.version.length - 1) exports.version = exports.version.substring(1).trim()
exports.version = exports.version.replace('-', '-c')
exports.builtinPlugins = [ exports.builtinPlugins = [
'terminus-core', 'terminus-core',
@@ -20,4 +21,4 @@ exports.bundledModules = [
'@angular', '@angular',
'@ng-bootstrap', '@ng-bootstrap',
] ]
exports.electronVersion = pkgInfo.devDependencies.electron exports.electronVersion = electronInfo.version

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "terminus-core", "name": "terminus-core",
"version": "1.0.0-alpha.55", "version": "1.0.68-c17-g8b64a81",
"description": "Terminus core", "description": "Terminus core",
"keywords": [ "keywords": [
"terminus-builtin-plugin" "terminus-builtin-plugin"
@@ -27,7 +27,7 @@
"electron-updater": "^2.8.9", "electron-updater": "^2.8.9",
"ng2-dnd": "^5.0.2", "ng2-dnd": "^5.0.2",
"ngx-perfect-scrollbar": "^6.0.0", "ngx-perfect-scrollbar": "^6.0.0",
"rage-edit-tmp": "^1.1.0", "rage-edit": "^1.2.0",
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"universal-analytics": "^0.4.17" "universal-analytics": "^0.4.17"
}, },

View File

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

View File

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

View File

@@ -6,5 +6,5 @@ export interface RecoveredTab {
} }
export abstract class TabRecoveryProvider { export abstract class TabRecoveryProvider {
abstract async recover (recoveryToken: any): Promise<RecoveredTab|null> abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
} }

View File

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

View File

@@ -69,7 +69,7 @@ export abstract class BaseTabComponent {
this.activity.next(false) this.activity.next(false)
} }
getRecoveryToken (): any { async getRecoveryToken (): Promise<any> {
return null return null
} }

View File

@@ -1,4 +1,4 @@
.icon(tabindex='0', [class.active]='model', (keyup.space)='click()') .icon(tabindex='0', [class.active]='model', (keyup.space)='click()')
i.fa.fa-square-o.off i.fas.fa-square.off
i.fa.fa-check-square.on i.fas.fa-check-square.on
.text {{text}} .text {{text}}

View File

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

View File

@@ -16,6 +16,7 @@ export class RenameTabModalComponent {
ngOnInit () { ngOnInit () {
setTimeout(() => { setTimeout(() => {
this.input.nativeElement.focus() this.input.nativeElement.focus()
this.input.nativeElement.select()
}, 250) }, 250)
} }

View File

@@ -14,10 +14,10 @@ div
footer.d-flex.align-items-center footer.d-flex.align-items-center
.btn-group.mr-auto .btn-group.mr-auto
button.btn.btn-secondary((click)='homeBase.openGitHub()') button.btn.btn-secondary((click)='homeBase.openGitHub()')
i.fa.fa-github i.fab.fa-github
span GitHub span GitHub
button.btn.btn-secondary((click)='homeBase.reportBug()') button.btn.btn-secondary((click)='homeBase.reportBug()')
i.fa.fa-bug i.fas.fa-bug
span Report a problem span Report a problem
.form-control-static.selectable.no-drag Version: {{homeBase.appVersion}} .form-control-static.selectable.no-drag Version: {{homeBase.appVersion}}

View File

@@ -17,7 +17,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
export class TabBodyComponent implements OnChanges { export class TabBodyComponent implements OnChanges {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
ngOnChanges (changes) { ngOnChanges (changes) {
if (changes.tab) { if (changes.tab) {

View File

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

View File

@@ -7,46 +7,33 @@ hotkeys:
- 'F11' - 'F11'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
- ['Ctrl-A', 'K'] toggle-last-tab: []
toggle-last-tab: rename-tab:
- ['Ctrl-A', 'A'] - 'Ctrl-Shift-R'
- ['Ctrl-A', 'Ctrl-A']
next-tab: next-tab:
- 'Ctrl-Shift-ArrowRight' - 'Ctrl-Shift-ArrowRight'
- ['Ctrl-A', 'N']
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:
- 'Ctrl-Shift-ArrowLeft' - 'Ctrl-Shift-ArrowLeft'
- ['Ctrl-A', 'P']
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
- ['Ctrl-A', '1']
tab-2: tab-2:
- 'Alt-2' - 'Alt-2'
- ['Ctrl-A', '2']
tab-3: tab-3:
- 'Alt-3' - 'Alt-3'
- ['Ctrl-A', '3']
tab-4: tab-4:
- 'Alt-4' - 'Alt-4'
- ['Ctrl-A', '4']
tab-5: tab-5:
- 'Alt-5' - 'Alt-5'
- ['Ctrl-A', '5']
tab-6: tab-6:
- 'Alt-6' - 'Alt-6'
- ['Ctrl-A', '6']
tab-7: tab-7:
- 'Alt-7' - 'Alt-7'
- ['Ctrl-A', '7']
tab-8: tab-8:
- 'Alt-8' - 'Alt-8'
- ['Ctrl-A', '8']
tab-9: tab-9:
- 'Alt-9' - 'Alt-9'
- ['Ctrl-A', '9']
tab-10: tab-10:
- 'Alt-0' - 'Alt-0'
- ['Ctrl-A', '0']
pluginBlacklist: ['ssh'] pluginBlacklist: ['ssh']

View File

@@ -8,6 +8,8 @@ hotkeys:
close-tab: close-tab:
- '⌘-W' - '⌘-W'
toggle-last-tab: [] toggle-last-tab: []
rename-tab:
- '⌘-R'
next-tab: next-tab:
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:

View File

@@ -7,46 +7,33 @@ hotkeys:
- 'F11' - 'F11'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
- ['Ctrl-A', 'K'] toggle-last-tab: []
toggle-last-tab: rename-tab:
- ['Ctrl-A', 'A'] - 'Ctrl-Shift-R'
- ['Ctrl-A', 'Ctrl-A']
next-tab: next-tab:
- 'Ctrl-Shift-ArrowRight' - 'Ctrl-Shift-ArrowRight'
- ['Ctrl-A', 'N']
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:
- 'Ctrl-Shift-ArrowLeft' - 'Ctrl-Shift-ArrowLeft'
- ['Ctrl-A', 'P']
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
- ['Ctrl-A', '1']
tab-2: tab-2:
- 'Alt-2' - 'Alt-2'
- ['Ctrl-A', '2']
tab-3: tab-3:
- 'Alt-3' - 'Alt-3'
- ['Ctrl-A', '3']
tab-4: tab-4:
- 'Alt-4' - 'Alt-4'
- ['Ctrl-A', '4']
tab-5: tab-5:
- 'Alt-5' - 'Alt-5'
- ['Ctrl-A', '5']
tab-6: tab-6:
- 'Alt-6' - 'Alt-6'
- ['Ctrl-A', '6']
tab-7: tab-7:
- 'Alt-7' - 'Alt-7'
- ['Ctrl-A', '7']
tab-8: tab-8:
- 'Alt-8' - 'Alt-8'
- ['Ctrl-A', '8']
tab-9: tab-9:
- 'Alt-9' - 'Alt-9'
- ['Ctrl-A', '9']
tab-10: tab-10:
- 'Alt-0' - 'Alt-0'
- ['Ctrl-A', '0']
pluginBlacklist: [] pluginBlacklist: []

View File

@@ -6,19 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd' import { DndModule } from 'ng2-dnd'
import { AppService } from './services/app.service' import { AppHotkeyProvider } from './services/hotkeys.service'
import { ConfigService } from './services/config.service'
import { ElectronService } from './services/electron.service'
import { HostAppService } from './services/hostApp.service'
import { LogService } from './services/log.service'
import { HomeBaseService } from './services/homeBase.service'
import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
import { DockingService } from './services/docking.service'
import { ShellIntegrationService } from './services/shellIntegration.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' import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component' import { CheckboxComponent } from './components/checkbox.component'
@@ -36,33 +24,25 @@ import { AutofocusDirective } from './directives/autofocus.directive'
import { HotkeyProvider } from './api/hotkeyProvider' import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider' import { ConfigProvider } from './api/configProvider'
import { Theme } from './api/theme' import { Theme } from './api/theme'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config' import { CoreConfigProvider } from './config'
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css' import 'ng2-dnd/bundles/style.css'
const PROVIDERS = [ const PROVIDERS = [
AppService,
ConfigService,
DockingService,
ElectronService,
HomeBaseService,
HostAppService,
HotkeysService,
LogService,
ShellIntegrationService,
TabRecoveryService,
ThemesService,
TouchbarService,
UpdaterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true }, { provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
{ provide: Theme, useClass: StandardTheme, multi: true }, { provide: Theme, useClass: StandardTheme, multi: true },
{ provide: Theme, useClass: StandardCompactTheme, multi: true }, { provide: Theme, useClass: StandardCompactTheme, multi: true },
{ provide: Theme, useClass: PaperTheme, multi: true }, { provide: Theme, useClass: PaperTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true }, { provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true }} { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
] ]
@NgModule({ @NgModule({

View File

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

View File

@@ -56,7 +56,7 @@ export class ConfigProxy {
return real[key] return real[key]
} else { } else {
if (isNonStructuralObjectMember(defaults[key])) { if (isNonStructuralObjectMember(defaults[key])) {
real[key] = {...defaults[key]} real[key] = { ...defaults[key] }
delete real[key].__nonStructural delete real[key].__nonStructural
return real[key] return real[key]
} else { } else {
@@ -74,7 +74,7 @@ export class ConfigProxy {
setValue (key: string, value: any) { } // tslint:disable-line setValue (key: string, value: any) { } // tslint:disable-line
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class ConfigService { export class ConfigService {
store: any store: any
restartRequested: boolean restartRequested: boolean

View File

@@ -8,7 +8,7 @@ export interface IScreen {
name: string name: string
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class DockingService { export class DockingService {
constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,

View File

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

View File

@@ -5,7 +5,7 @@ import { ConfigService } from './config.service'
import ua = require('universal-analytics') import ua = require('universal-analytics')
import uuidv4 = require('uuid/v4') import uuidv4 = require('uuid/v4')
@Injectable() @Injectable({ providedIn: 'root' })
export class HomeBaseService { export class HomeBaseService {
appVersion: string appVersion: string

View File

@@ -16,7 +16,7 @@ export interface Bounds {
height: number height: number
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class HostAppService { export class HostAppService {
platform: Platform platform: Platform
nodePlatform: string nodePlatform: string
@@ -27,8 +27,11 @@ export class HostAppService {
private cliOpenDirectory = new Subject<string>() private cliOpenDirectory = new Subject<string>()
private cliRunCommand = new Subject<string[]>() private cliRunCommand = new Subject<string[]>()
private cliPaste = new Subject<string>() private cliPaste = new Subject<string>()
private cliOpenProfile = new Subject<string>()
private configChangeBroadcast = new Subject<void>() private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>() private windowCloseRequest = new Subject<void>()
private windowMoved = new Subject<void>()
private displayMetricsChanged = new Subject<void>()
private logger: Logger private logger: Logger
private windowId: number private windowId: number
@@ -37,8 +40,11 @@ export class HostAppService {
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory } get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand } get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
get cliPaste$ (): Observable<string> { return this.cliPaste } get cliPaste$ (): Observable<string> { return this.cliPaste }
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast } get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest } get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
get windowMoved$ (): Observable<void> { return this.windowMoved }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
constructor ( constructor (
private zone: NgZone, private zone: NgZone,
@@ -78,6 +84,14 @@ export class HostAppService {
this.zone.run(() => this.windowCloseRequest.next()) this.zone.run(() => this.windowCloseRequest.next())
}) })
electron.ipcRenderer.on('host:window-moved', () => {
this.zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next())
})
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => { electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => {
this.logger.info('Second instance', argv) this.logger.info('Second instance', argv)
const op = argv._[0] const op = argv._[0]
@@ -91,6 +105,8 @@ export class HostAppService {
text = shellEscape([text]) text = shellEscape([text])
} }
this.cliPaste.next(text) this.cliPaste.next(text)
} else if (op === 'profile') {
this.cliOpenProfile.next(argv.profileName)
} else { } else {
this.secondInstance.next() this.secondInstance.next()
} }

View File

@@ -17,7 +17,7 @@ interface EventBufferEntry {
time: number, time: number,
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class HotkeysService { export class HotkeysService {
key = new EventEmitter<NativeKeyEvent>() key = new EventEmitter<NativeKeyEvent>()
matchedHotkey = new EventEmitter<string>() matchedHotkey = new EventEmitter<string>()
@@ -80,8 +80,8 @@ export class HotkeysService {
} }
getCurrentKeystrokes (): string[] { getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT ) this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event)) return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
} }
registerGlobalHotkey () { registerGlobalHotkey () {
@@ -215,6 +215,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'toggle-fullscreen', id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode', name: 'Toggle fullscreen mode',
}, },
{
id: 'rename-tab',
name: 'Rename Tab',
},
{ {
id: 'close-tab', id: 'close-tab',
name: 'Close tab', name: 'Close tab',

View File

@@ -23,7 +23,7 @@ const initializeWinston = (electron: ElectronService) => {
colorize: false colorize: false
}), }),
new winston.transports.Console({ new winston.transports.Console({
level: 'info', level: 'debug',
handleExceptions: false, handleExceptions: false,
json: false, json: false,
colorize: true colorize: true
@@ -53,7 +53,7 @@ export class Logger {
log (...args: any[]) { this.doLog('log', ...args) } log (...args: any[]) { this.doLog('log', ...args) }
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class LogService { export class LogService {
private log: any private log: any

View File

@@ -1,12 +1,12 @@
import * as path from 'path' import * as path from 'path'
import * as fs from 'mz/fs' import * as fs from 'mz/fs'
import { Registry } from 'rage-edit-tmp' import { Registry } from 'rage-edit'
import { exec } from 'mz/child_process' import { exec } from 'mz/child_process'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { HostAppService, Platform } from './hostApp.service' import { HostAppService, Platform } from './hostApp.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class ShellIntegrationService { export class ShellIntegrationService {
private automatorWorkflows = ['Open Terminus here.workflow', 'Paste path into Terminus.workflow'] private automatorWorkflows = ['Open Terminus here.workflow', 'Paste path into Terminus.workflow']
private automatorWorkflowsLocation: string private automatorWorkflowsLocation: string
@@ -48,9 +48,9 @@ export class ShellIntegrationService {
async isInstalled (): Promise<boolean> { async isInstalled (): Promise<boolean> {
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
return await fs.exists(path.join(this.automatorWorkflowsDestination, this.automatorWorkflows[0])) return fs.exists(path.join(this.automatorWorkflowsDestination, this.automatorWorkflows[0]))
} else if (this.hostApp.platform === Platform.Windows) { } else if (this.hostApp.platform === Platform.Windows) {
return await Registry.has(this.registryKeys[0].path) return Registry.has(this.registryKeys[0].path)
} }
return true return true
} }

View File

@@ -2,52 +2,56 @@ import { Injectable, Inject } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery' import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service' import { Logger, LogService } from '../services/log.service'
import { AppService } from '../services/app.service'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class TabRecoveryService { export class TabRecoveryService {
logger: Logger logger: Logger
constructor ( constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], @Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private app: AppService,
private config: ConfigService, private config: ConfigService,
log: LogService log: LogService
) { ) {
this.logger = log.create('tabRecovery') this.logger = log.create('tabRecovery')
app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs)
})
} }
saveTabs (tabs: BaseTabComponent[]) { async saveTabs (tabs: BaseTabComponent[]) {
window.localStorage.tabsRecovery = JSON.stringify( window.localStorage.tabsRecovery = JSON.stringify(
tabs await Promise.all(
.map((tab) => tab.getRecoveryToken()) tabs
.filter((token) => !!token) .map(tab => tab.getRecoveryToken())
.filter(token => !!token)
)
) )
} }
async recoverTabs (): Promise<void> { async recoverTab (token: any): Promise<RecoveredTab> {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try {
let tab = await provider.recover(token)
if (tab) {
return tab
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
return null
}
async recoverTabs (): Promise<RecoveredTab[]> {
if (window.localStorage.tabsRecovery) { if (window.localStorage.tabsRecovery) {
let tabs: RecoveredTab[] = [] let tabs: RecoveredTab[] = []
for (let token of JSON.parse(window.localStorage.tabsRecovery)) { for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) { let tab = await this.recoverTab(token)
try { if (tab) {
let tab = await provider.recover(token) tabs.push(tab)
if (tab) {
tabs.push(tab)
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
} }
} }
tabs.forEach(tab => { return tabs
this.app.openNewTab(tab.type, tab.options)
})
} }
return []
} }
} }

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { Theme } from '../api/theme' import { Theme } from '../api/theme'
@Injectable() @Injectable({ providedIn: 'root' })
export class ThemesService { export class ThemesService {
private styleElement: HTMLElement = null private styleElement: HTMLElement = null

View File

@@ -6,7 +6,7 @@ import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service' import { HostAppService } from './hostApp.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api' import { IToolbarButton, ToolbarButtonProvider } from '../api'
@Injectable() @Injectable({ providedIn: 'root' })
export class TouchbarService { export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl private tabsSegmentedControl: TouchBarSegmentedControl
private tabSegments: SegmentedControlSegment[] = [] private tabSegments: SegmentedControlSegment[] = []
@@ -49,8 +49,8 @@ export class TouchbarService {
let touchBar = new this.electron.TouchBar({ let touchBar = new this.electron.TouchBar({
items: [ items: [
this.tabsSegmentedControl, this.tabsSegmentedControl,
new this.electron.TouchBar.TouchBarSpacer({size: 'flexible'}), new this.electron.TouchBar.TouchBarSpacer({ size: 'flexible' }),
new this.electron.TouchBar.TouchBarSpacer({size: 'small'}), new this.electron.TouchBar.TouchBarSpacer({ size: 'small' }),
...buttons.map(button => this.getButton(button)) ...buttons.map(button => this.getButton(button))
] ]
}) })

View File

@@ -6,7 +6,7 @@ import { ElectronService } from './electron.service'
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest' const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
@Injectable() @Injectable({ providedIn: 'root' })
export class UpdaterService { export class UpdaterService {
private logger: Logger private logger: Logger
private downloaded: Promise<boolean> private downloaded: Promise<boolean>

View File

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

View File

@@ -51,6 +51,7 @@ $input-disabled-bg: #333;
$input-color: $body-color; $input-color: $body-color;
$input-color-placeholder: #333; $input-color-placeholder: #333;
$input-border-color: #344; $input-border-color: #344;
$input-border-width: 0;
//$input-box-shadow: inset 0 1px 1px rgba($black,.075); //$input-box-shadow: inset 0 1px 1px rgba($black,.075);
$input-border-radius: 0; $input-border-radius: 0;
$custom-select-border-radius: 0; $custom-select-border-radius: 0;
@@ -70,7 +71,7 @@ $popover-bg: $body-bg;
$dropdown-bg: $body-bg; $dropdown-bg: $body-bg;
$dropdown-link-color: $body-color; $dropdown-link-color: $body-color;
$dropdown-link-hover-color: #333; $dropdown-link-hover-color: white;
$dropdown-link-hover-bg: $body-bg2; $dropdown-link-hover-bg: $body-bg2;
//$dropdown-link-active-color: $component-active-color; //$dropdown-link-active-color: $component-active-color;
//$dropdown-link-active-bg: $component-active-bg; //$dropdown-link-active-bg: $component-active-bg;
@@ -346,6 +347,15 @@ ngb-tabset .tab-content {
} }
} }
.list-group.list-group-flush .list-group-item:not(.list-group-item-action) {
background: transparent;
border-color: rgba(0, 0, 0, 0.2);
&:not(:last-child) {
border-bottom: none;
}
}
select.form-control { select.form-control {
-webkit-appearance: none; -webkit-appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>"); background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>");
@@ -362,6 +372,15 @@ toggle.active .body .toggle {
background: $blue; background: $blue;
} }
.modal .modal-footer {
background: rgba(0, 0, 0, .25);
.btn {
font-weight: bold;
padding: 0.375rem 1.5rem;
}
}
.list-group-item svg { .list-group-item svg {
fill: white; fill: white;
fill-opacity: 0.75; fill-opacity: 0.75;

View File

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

View File

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

View File

@@ -486,10 +486,10 @@ qs@~6.5.1:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
rage-edit-tmp@^1.1.0: rage-edit@^1.2.0:
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/rage-edit-tmp/-/rage-edit-tmp-1.1.0.tgz#fc5d76716d2fe2cf97dcafbf3e26753e3a08e3b2" resolved "https://registry.yarnpkg.com/rage-edit/-/rage-edit-1.2.0.tgz#991860a60fef934d8a6d0f057e55786b02f94a2b"
integrity sha512-lR97QHY5WSf9orInMJhPqUbenkdiy7QbXUoRMI+wBZGyAPkxNwgo7h6ojq634QrBf/kQo3mVXYjuD3ZYraNaZQ== integrity sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==
request@2.86.0: request@2.86.0:
version "2.86.0" version "2.86.0"

View File

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

View File

@@ -2,57 +2,61 @@
strong Error in {{erroredPlugin}}: strong Error in {{erroredPlugin}}:
pre {{errorMessage}} pre {{errorMessage}}
button.btn.btn-outline-info.btn-sm.pull-right((click)='openPluginsFolder()')
i.fa.fa-folder .d-flex
span Plugins folder h3.mb-1 Installed
button.btn.btn-outline-info.btn-sm.ml-auto((click)='openPluginsFolder()')
i.fas.fa-folder
span Plugins folder
h3.mb-1 Installed .list-group.list-group-flush.mt-2
.list-group-item.d-flex.align-items-center(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
.mr-auto.d-flex.flex-column
div
strong {{plugin.name}}
small.text-muted.ml-1(*ngIf='!plugin.isBuiltin') {{plugin.version}} / {{plugin.author}}
small.text-warning.ml-1(*ngIf='config.store.pluginBlacklist.includes(plugin.name)') Disabled
a.text-muted.mb-0((click)='showPluginInfo(plugin)')
small {{plugin.description}}
.mb-3.d-flex.w-100.align-items-center(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"') button.btn.btn-primary.ml-2(
button.btn.btn-outline-danger.active.mr-2( *ngIf='npmInstalled && knownUpgrades[plugin.name]',
*ngIf='config.store.pluginBlacklist.includes(plugin.name)', (click)='upgradePlugin(plugin)',
(click)='enablePlugin(plugin)' [disabled]='busy[plugin.name] != undefined'
) )
i.fa.fa-fw.fa-pause i.fas.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing')
button.btn.btn-outline-secondary.mr-2( i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)', span Upgrade ({{knownUpgrades[plugin.name].version}})
(click)='disablePlugin(plugin)'
)
i.fa.fa-fw.fa-check
.mr-auto.d-flex.flex-column button.btn.btn-primary.ml-2(
div *ngIf='config.store.pluginBlacklist.includes(plugin.name)',
strong {{plugin.name}} (click)='enablePlugin(plugin)'
small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}} )
a.text-muted.mb-0((click)='showPluginInfo(plugin)') i.fas.fa-fw.fa-play
small {{plugin.description}}
button.btn.btn-secondary.ml-2(
button.btn.btn-primary.ml-2( *ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
*ngIf='npmInstalled && knownUpgrades[plugin.name]', (click)='disablePlugin(plugin)'
(click)='upgradePlugin(plugin)', )
[disabled]='busy[plugin.name] != undefined' i.fas.fa-fw.fa-pause
)
i.fa.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing') button.btn.btn-danger.ml-2(
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing') (click)='uninstallPlugin(plugin)',
span Upgrade ({{knownUpgrades[plugin.name].version}}) *ngIf='!plugin.isBuiltin && npmInstalled',
[disabled]='busy[plugin.name] != undefined'
button.btn.btn-outline-danger.ml-2( )
(click)='uninstallPlugin(plugin)', i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
*ngIf='!plugin.isBuiltin && npmInstalled', i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
[disabled]='busy[plugin.name] != undefined'
)
i.fa.fa-fw.fa-trash-o(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
.text-center.mt-5(*ngIf='npmMissing') .text-center.mt-5(*ngIf='npmMissing')
h4 npm not installed h4 npm not installed
p.mb-2 npm is required to install Terminus plugins. p.mb-2 npm is required to install Terminus plugins.
.btn-group .btn-group
button.btn.btn-outline-primary((click)='downloadNPM()') button.btn.btn-outline-primary((click)='downloadNPM()')
i.fa.fa-download i.fas.fa-download
span Get npm span Get npm
button.btn.btn-outline-info((click)='checkNPM()') button.btn.btn-outline-info((click)='checkNPM()')
i.fa.fa-refresh i.fas.fa-refresh
span Try again span Try again
div(*ngIf='npmInstalled') div(*ngIf='npmInstalled')
@@ -61,8 +65,8 @@ div(*ngIf='npmInstalled')
.input-group.mb-3 .input-group.mb-3
.input-group-prepend .input-group-prepend
.input-group-text .input-group-text
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='!availablePluginsReady') i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fa.fa-fw.fa-search(*ngIf='availablePluginsReady') i.fas.fa-fw.fa-search(*ngIf='availablePluginsReady')
input.form-control( input.form-control(
type='text', type='text',
[(ngModel)]='_1', [(ngModel)]='_1',
@@ -71,19 +75,19 @@ div(*ngIf='npmInstalled')
) )
.mb-4(*ngIf='availablePlugins$') .list-group.list-group-flush.mb-4(*ngIf='availablePlugins$')
ng-container(*ngFor='let plugin of (availablePlugins$|async|orderBy:"name")') ng-container(*ngFor='let plugin of (availablePlugins$|async|orderBy:"name")')
.d-flex.w-100.align-items-center.mb-3(*ngIf='!isAlreadyInstalled(plugin)') .list-group-item.d-flex.align-items-center(*ngIf='!isAlreadyInstalled(plugin)')
button.btn.btn-primary.mr-2( button.btn.btn-primary.mr-3(
(click)='installPlugin(plugin)', (click)='installPlugin(plugin)',
[disabled]='busy[plugin.name] != undefined' [disabled]='busy[plugin.name] != undefined'
) )
i.fa.fa-fw.fa-download(*ngIf='busy[plugin.name] != BusyState.Installing') i.fas.fa-fw.fa-download(*ngIf='busy[plugin.name] != BusyState.Installing')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing') i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
div((click)='showPluginInfo(plugin)') div((click)='showPluginInfo(plugin)')
div div
strong {{plugin.name}} strong {{plugin.name}}
small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}} small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}}
i.fa.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official') i.fas.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official')
small.text-muted {{plugin.description}} small.text-muted {{plugin.description}}

View File

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

View File

@@ -22,7 +22,6 @@ import { PluginsSettingsTabProvider } from './settings'
providers: [ providers: [
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
{ provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true }, { provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true },
PluginManagerService,
], ],
entryComponents: [ entryComponents: [
PluginsSettingsTabComponent, PluginsSettingsTabComponent,

View File

@@ -23,7 +23,7 @@ export interface IPluginInfo {
path?: string path?: string
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class PluginManagerService { export class PluginManagerService {
logger: Logger logger: Logger
builtinPluginsPath: string = (window as any).builtinPluginsPath builtinPluginsPath: string = (window as any).builtinPluginsPath
@@ -48,7 +48,7 @@ export class PluginManagerService {
return return
} }
if (this.hostApp.platform !== Platform.Windows) { if (this.hostApp.platform !== Platform.Windows) {
this.envPath = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim() this.envPath = (await exec('$SHELL -i -c \'echo $PATH\''))[0].toString().trim()
let searchPaths = this.envPath.split(':') let searchPaths = this.envPath.split(':')
for (let searchPath of searchPaths) { for (let searchPath of searchPaths) {
if (await fs.exists(path.join(searchPath, 'npm'))) { if (await fs.exists(path.join(searchPath, 'npm'))) {

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
ngb-tabset.vertical(type='pills', [activeId]='activeTab') ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='application') ngb-tab(id='application')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
i.fas.fa-fw.fa-window-maximize.mr-2
| Application | Application
ng-template(ngbTabContent) ng-template(ngbTabContent)
.d-flex.align-items-center.mb-4 .d-flex.align-items-center.mb-4
@@ -12,11 +13,11 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.text-muted.mr-auto {{homeBase.appVersion}} .text-muted.mr-auto {{homeBase.appVersion}}
button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()') button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()')
i.fa.fa-github i.fab.fa-github
span GitHub span GitHub
button.btn.btn-secondary((click)='homeBase.reportBug()') button.btn.btn-secondary((click)='homeBase.reportBug()')
i.fa.fa-bug i.fas.fa-bug
span Report a problem span Report a problem
.form-line(*ngIf='!isShellIntegrationInstalled') .form-line(*ngIf='!isShellIntegrationInstalled')
@@ -24,7 +25,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.title Shell integration .title Shell integration
.description Allows quickly opening a terminal in the selected folder .description Allows quickly opening a terminal in the selected folder
button.btn.btn-primary((click)='installShellIntegration()') button.btn.btn-primary((click)='installShellIntegration()')
i.fa.fa-check i.fas.fa-check
span Install span Install
.form-line .form-line
@@ -225,7 +226,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.title Debugging .title Debugging
button.btn.btn-secondary((click)='hostApp.openDevTools()') button.btn.btn-secondary((click)='hostApp.openDevTools()')
i.fa.fa-bug i.fas.fa-bug
span Open DevTools span Open DevTools
.form-line .form-line
@@ -247,6 +248,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='hotkeys') ngb-tab(id='hotkeys')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
i.fas.fa-fw.fa-keyboard.mr-2
| Hotkeys | Hotkeys
ng-template(ngbTabContent) ng-template(ngbTabContent)
h3.mb-3 Hotkeys h3.mb-3 Hotkeys
@@ -254,7 +256,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.input-group.mb-4 .input-group.mb-4
.input-group-prepend .input-group-prepend
.input-group-text .input-group-text
i.fa.fa-fw.fa-search i.fas.fa-fw.fa-search
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter') input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
.form-group .form-group
@@ -274,6 +276,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id') ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
| {{provider.title}} | {{provider.title}}
ng-template(ngbTabContent) ng-template(ngbTabContent)
settings-tab-body([provider]='provider') settings-tab-body([provider]='provider')
@@ -281,6 +284,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
ngb-tab(id='config-file') ngb-tab(id='config-file')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
i.fas.fa-fw.fa-code.mr-2
| Config file | Config file
ng-template.test(ngbTabContent) ng-template.test(ngbTabContent)
.d-flex.flex-column.w-100.h-100 .d-flex.flex-column.w-100.h-100
@@ -298,8 +302,8 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
) )
.mt-3 .mt-3
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()') button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
i.fa.fa-check.mr-2 i.fas.fa-check.mr-2
| Save and apply | Save and apply
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()') button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
i.fa.fa-warning.mr-2 i.fas.fa-exclamation-triangle.mr-2
| Invalid syntax | Invalid syntax

View File

@@ -2,12 +2,12 @@ import * as yaml from 'js-yaml'
import * as os from 'os' import * as os from 'os'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { Component, Inject, Input, HostBinding } from '@angular/core' import { Component, Inject, Input, HostBinding } from '@angular/core'
import { HotkeysService } from 'terminus-core'
import { import {
ElectronService, ElectronService,
DockingService, DockingService,
ConfigService, ConfigService,
IHotkeyDescription, IHotkeyDescription,
HotkeysService,
BaseTabComponent, BaseTabComponent,
Theme, Theme,
HostAppService, HostAppService,
@@ -80,7 +80,7 @@ export class SettingsTabComponent extends BaseTabComponent {
this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled() this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
} }
getRecoveryToken (): any { async getRecoveryToken (): Promise<any> {
return { type: 'app:settings' } return { type: 'app:settings' }
} }

View File

@@ -7,7 +7,7 @@ import { SettingsTabProvider } from '../api'
}) })
export class SettingsTabBodyComponent { export class SettingsTabBodyComponent {
@Input() provider: SettingsTabProvider @Input() provider: SettingsTabProvider
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
component: ComponentRef<Component> component: ComponentRef<Component>
constructor (private componentFactoryResolver: ComponentFactoryResolver) { } constructor (private componentFactoryResolver: ComponentFactoryResolver) { }

View File

@@ -4,8 +4,7 @@ import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgPipesModule } from 'ngx-pipes' import { NgPipesModule } from 'ngx-pipes'
import { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core' import TerminusCorePlugin, { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
import TerminusCorePlugin from 'terminus-core'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component' import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@
) )
.input-group-btn .input-group-btn
button.btn.btn-secondary((click)='selectPrivateKey()') button.btn.btn-secondary((click)='selectPrivateKey()')
i.fa.fa-folder-open i.fas.fa-folder-open
ngb-tab(id='advanced') ngb-tab(id='advanced')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
@@ -85,6 +85,27 @@
placeholder='20000', placeholder='20000',
[(ngModel)]='connection.readyTimeout', [(ngModel)]='connection.readyTimeout',
) )
.form-group
label Ciphers
div(*ngFor='let alg of supportedAlgorithms.cipher')
checkbox([text]='alg', [(ngModel)]='algorithms.cipher[alg]')
.form-group
label Key exchange
div(*ngFor='let alg of supportedAlgorithms.kex')
checkbox([text]='alg', [(ngModel)]='algorithms.kex[alg]')
.form-group
label HMAC
div(*ngFor='let alg of supportedAlgorithms.hmac')
checkbox([text]='alg', [(ngModel)]='algorithms.hmac[alg]')
.form-group
label Host key
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
ngb-tab(id='scripts') ngb-tab(id='scripts')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
@@ -119,11 +140,11 @@
td td
.input-group.flex-nowrap .input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
i.fa.fa-arrow-up i.fas.fa-arrow-up
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
i.fa.fa-arrow-down i.fas.fa-arrow-down
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
i.fa.fa-trash-o i.fas.fa-trash
tr tr
td td
input.form-control( input.form-control(
@@ -148,9 +169,9 @@
td td
.input-group.flex-nowrap .input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='addScript()') button.btn.btn-outline-info.ml-0((click)='addScript()')
i.fa.fa-check i.fas.fa-check
button.btn.btn-outline-danger.ml-0((click)='clearScript()') button.btn.btn-outline-danger.ml-0((click)='clearScript()')
i.fa.fa-trash-o i.fas.fa-trash
.modal-footer .modal-footer
button.btn.btn-outline-primary((click)='save()') Save button.btn.btn-outline-primary((click)='save()') Save

View File

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

View File

@@ -10,10 +10,10 @@
.list-group.mt-3(*ngIf='lastConnection') .list-group.mt-3(*ngIf='lastConnection')
a.list-group-item.list-group-item-action.d-flex.align-items-center((click)='connect(lastConnection)') a.list-group-item.list-group-item-action.d-flex.align-items-center((click)='connect(lastConnection)')
i.fa.fa-fw.fa-history i.fas.fa-fw.fa-history
.mr-auto {{lastConnection.name}} .mr-auto {{lastConnection.name}}
button.btn.btn-outline-danger.btn-sm((click)='clearLastConnection(); $event.stopPropagation()') button.btn.btn-outline-danger.btn-sm((click)='clearLastConnection(); $event.stopPropagation()')
i.fa.fa-trash-o i.fas.fa-trash
.list-group.mt-3.connections-list(*ngIf='childGroups.length') .list-group.mt-3.connections-list(*ngIf='childGroups.length')
ng-container(*ngFor='let group of childGroups') ng-container(*ngFor='let group of childGroups')
@@ -27,4 +27,6 @@
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center( .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections', *ngFor='let connection of group.connections',
(click)='connect(connection)' (click)='connect(connection)'
) {{connection.name}} )
.mr-2 {{connection.name}}
.text-muted {{connection.host}}

View File

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

View File

@@ -1,25 +1,28 @@
h3 Connections h3 Connections
.list-group.mt-3.mb-3 .list-group.list-group-flush.mt-3.mb-3
ng-container(*ngFor='let group of childGroups') ng-container(*ngFor='let group of childGroups')
.list-group-item.list-group-item-action.d-flex.align-items-center((click)='groupCollapsed[group.name] = !groupCollapsed[group.name]') .list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
)
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]') .fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]') .fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
span.ml-3.mr-auto {{group.name || "Ungrouped"}} span.ml-3.mr-auto {{group.name || "Ungrouped"}}
button.btn.btn-outline-info.ml-2((click)='editGroup(group)') button.btn.btn-outline-info.ml-2((click)='editGroup(group)')
i.fa.fa-pencil i.fas.fa-edit
button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)') button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)')
i.fa.fa-trash-o i.fas.fa-trash
ng-container(*ngIf='!groupCollapsed[group.name]') ng-container(*ngIf='!groupCollapsed[group.name]')
.list-group-item.pl-5.d-flex.align-items-center(*ngFor='let connection of group.connections') .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections',
(click)='editConnection(connection)'
)
.mr-auto .mr-auto
div {{connection.name}} div {{connection.name}}
.text-muted {{connection.host}} .text-muted {{connection.host}}
button.btn.btn-outline-info.ml-2((click)='editConnection(connection)') button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)')
i.fa.fa-pencil i.fas.fa-trash
button.btn.btn-outline-danger.ml-1((click)='deleteConnection(connection)')
i.fa.fa-trash-o
button.btn.btn-outline-primary((click)='createConnection()') button.btn.btn-primary((click)='createConnection()')
div.fa.fa-fw.fa-globe i.fas.fa-fw.fa-plus
span.ml-2 Add connection span.ml-2 Add connection

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ try {
} }
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class PasswordStorageService { export class PasswordStorageService {
constructor ( constructor (
private zone: NgZone, private zone: NgZone,

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ module.exports = {
libraryTarget: 'umd', libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-ssh:///[resource-path]', devtoolModuleFilenameTemplate: 'webpack-terminus-ssh:///[resource-path]',
}, },
mode: process.env.DEV ? 'development' : 'production', mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{ optimization:{
minimize: false, minimize: false,
}, },
@@ -44,10 +44,12 @@ module.exports = {
externals: [ externals: [
'fs', 'fs',
'node-ssh', 'node-ssh',
'ssh2-streams',
'xkeychain', 'xkeychain',
'wincredmgr', 'wincredmgr',
'path', 'path',
'ngx-toastr', 'ngx-toastr',
'windows-process-tree/build/Release/windows_process_tree.node',
/^rxjs/, /^rxjs/,
/^@angular/, /^@angular/,
/^@ng-bootstrap/, /^@ng-bootstrap/,

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,11 +1,10 @@
import { Observable } from 'rxjs' import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
import { TerminalTabComponent } from './components/terminalTab.component'
export abstract class TerminalDecorator { export abstract class TerminalDecorator {
// tslint:disable-next-line no-empty // tslint:disable-next-line no-empty
attach (_terminal: TerminalTabComponent): void { } attach (_terminal: BaseTerminalTabComponent): void { }
// tslint:disable-next-line no-empty // tslint:disable-next-line no-empty
detach (_terminal: TerminalTabComponent): void { } detach (_terminal: BaseTerminalTabComponent): void { }
} }
export interface ResizeEvent { export interface ResizeEvent {
@@ -15,25 +14,19 @@ export interface ResizeEvent {
export interface SessionOptions { export interface SessionOptions {
name?: string name?: string
command?: string command: string
args?: string[] args: string[]
cwd?: string cwd?: string
env?: any env?: any
width?: number width?: number
height?: number height?: number
recoveryId?: string
recoveredTruePID$?: Observable<number>
pauseAfterExit?: boolean pauseAfterExit?: boolean
runAsAdministrator?: boolean
} }
export abstract class SessionPersistenceProvider { export interface Profile {
abstract id: string name: string,
abstract displayName: string sessionOptions: SessionOptions,
abstract isAvailable (): boolean
abstract async attachSession (recoveryId: any): Promise<SessionOptions>
abstract async startSession (options: SessionOptions): Promise<any>
abstract async terminateSession (recoveryId: string): Promise<void>
} }
export interface ITerminalColorScheme { export interface ITerminalColorScheme {
@@ -48,6 +41,12 @@ export abstract class TerminalColorSchemeProvider {
abstract async getSchemes (): Promise<ITerminalColorScheme[]> abstract async getSchemes (): Promise<ITerminalColorScheme[]>
} }
export abstract class TerminalContextMenuItemProvider {
weight: number
abstract async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
}
export interface IShell { export interface IShell {
id: string id: string
name?: string name?: string

View File

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

View File

@@ -1,6 +1,6 @@
h3.mb-3 Appearance h3.mb-3 Appearance
.row .d-flex
.col-md-6 .mr-5
.form-line .form-line
.header .header
.title Frontend .title Frontend
@@ -26,6 +26,7 @@ h3.mb-3 Appearance
) )
input.form-control.w-25( input.form-control.w-25(
type='number', type='number',
max='48',
[(ngModel)]='config.store.terminal.fontSize', [(ngModel)]='config.store.terminal.fontSize',
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
@@ -57,7 +58,7 @@ h3.mb-3 Appearance
(click)='deleteScheme(config.store.terminal.colorScheme)', (click)='deleteScheme(config.store.terminal.colorScheme)',
*ngIf='isCustomScheme(config.store.terminal.colorScheme)' *ngIf='isCustomScheme(config.store.terminal.colorScheme)'
) )
i.fa.fa-trash-o i.fas.fa-trash
.form-group(*ngIf='editingColorScheme') .form-group(*ngIf='editingColorScheme')
label Editing label Editing
@@ -91,7 +92,7 @@ h3.mb-3 Appearance
[title]='idx', [title]='idx',
) )
.col-md-6 div
.form-group .form-group
.appearance-preview( .appearance-preview(
[style.font-family]='config.store.terminal.font', [style.font-family]='config.store.terminal.font',

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
.modal-body
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.name',
)
.form-group
label Command
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.command',
)
.form-group
label Arguments
.input-group(
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
)
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.args[i]',
)
.input-group-btn
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
i.fas.fa-trash
.mt-2
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
i.fas.fa-plus.mr-2
| Add
.form-line(*ngIf='uac.isAvailable')
.header
.title Run as administrator
toggle(
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
)
.form-group
label Working directory
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.cwd',
)
.form-group
label Environment
environment-editor(
type='text',
[(model)]='profile.sessionOptions.env',
)
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save
button.btn.btn-outline-danger((click)='cancel()') Cancel

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { UACService } from '../services/uac.service'
import { Profile } from '../api'
@Component({
template: require('./editProfileModal.component.pug'),
})
export class EditProfileModalComponent {
profile: Profile
constructor (
public uac: UACService,
private modalInstance: NgbActiveModal,
) {
}
ngOnInit () {
this.profile.sessionOptions.env = this.profile.sessionOptions.env || {}
this.profile.sessionOptions.args = this.profile.sessionOptions.args || []
}
save () {
this.modalInstance.close(this.profile)
}
cancel () {
this.modalInstance.dismiss()
}
trackByIndex (index) {
return index
}
}

View File

@@ -0,0 +1,12 @@
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
.input-group.w-50
input.form-control([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
.input-group-append
.input-group-text =
input.form-control.w-50.mr-1([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
i.fas.fa-trash
button.btn.btn-secondary((click)='addEnvironmentVar()')
i.fas.fa-plus.mr-2
span Add

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

Some files were not shown because too many files have changed in this diff Show More