mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-31 22:46:58 +00:00
Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d0ddd82906 | ||
![]() |
8a5846ff81 | ||
![]() |
298209b03a | ||
![]() |
dc03a7f135 | ||
![]() |
2d357d0ed2 | ||
![]() |
58d2590495 | ||
![]() |
0c892225f3 | ||
![]() |
851d92a140 | ||
![]() |
665ce2f022 | ||
![]() |
5ea1ff1ea5 | ||
![]() |
5ed92342ea | ||
![]() |
d9843c4ff0 | ||
![]() |
448371dffa | ||
![]() |
425c288aa0 | ||
![]() |
09b118987e | ||
![]() |
70dbb6bfcc | ||
![]() |
4b55f9cd97 | ||
![]() |
fbb6614553 | ||
![]() |
8d849b40a2 | ||
![]() |
5e678c15f3 | ||
![]() |
6642be3050 | ||
![]() |
9998fa97f7 | ||
![]() |
2b3113b67d | ||
![]() |
f933c29e5e | ||
![]() |
a908d737a4 | ||
![]() |
469b4e4f44 | ||
![]() |
13b31d16e8 | ||
![]() |
afa2eb3a6e | ||
![]() |
01eb26e5c1 | ||
![]() |
b967a1ecfa | ||
![]() |
dfe91e98c3 | ||
![]() |
6706aa67eb | ||
![]() |
15fe4f34f9 | ||
![]() |
9b7c446fd7 | ||
![]() |
9225239433 | ||
![]() |
e764b22698 | ||
![]() |
a4d88e4631 | ||
![]() |
20da700ad4 | ||
![]() |
e4a4937023 | ||
![]() |
822b60b454 | ||
![]() |
72a024c713 | ||
![]() |
6132881b8a | ||
![]() |
1ac22ec563 | ||
![]() |
ca68905b05 | ||
![]() |
2470f5f941 | ||
![]() |
fd1ea4fc49 | ||
![]() |
3f8b933d05 | ||
![]() |
3b13f0d73f | ||
![]() |
a261efbc01 | ||
![]() |
04d4474648 | ||
![]() |
4a50c84cfe | ||
![]() |
9ca091e592 | ||
![]() |
fa56f30f63 | ||
![]() |
9c19307181 | ||
![]() |
bfd34df41e | ||
![]() |
26ee36458d | ||
![]() |
e99b83dfdc | ||
![]() |
ceb75323fe | ||
![]() |
bfb6417865 | ||
![]() |
498564be9a | ||
![]() |
6c38aa4008 | ||
![]() |
32aaa3d0ff | ||
![]() |
7ce5d647da | ||
![]() |
bf0be7fa0e | ||
![]() |
a10eb5ff90 | ||
![]() |
c6a27d8893 | ||
![]() |
7ee1fb4b76 | ||
![]() |
e0f9f558f1 | ||
![]() |
d511bb9fc0 | ||
![]() |
15f99c8ae8 | ||
![]() |
1027fbfb60 | ||
![]() |
d89abde860 | ||
![]() |
1d76f6b056 | ||
![]() |
f09fb735a7 | ||
![]() |
9ae92ef88c | ||
![]() |
b58d6e1bfd | ||
![]() |
a3a5da8550 | ||
![]() |
178a2e34c6 | ||
![]() |
b39276feca | ||
![]() |
2c40f0dddc |
@@ -79,6 +79,7 @@ rules:
|
||||
args: after-used
|
||||
argsIgnorePattern: ^_
|
||||
no-undef: error
|
||||
no-var: error
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
run: scripts/build-macos.js
|
||||
if: github.repository == 'Eugeny/terminus' && github.event_name == 'push'
|
||||
env:
|
||||
DEBUG: electron-builder,electron-builder:*
|
||||
#DEBUG: electron-builder,electron-builder:*
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
mkdir artifact-setup
|
||||
mv dist/*-setup.exe artifact-setup/
|
||||
mkdir artifact-portable
|
||||
mv dist/*-portable.exe artifact-portable/
|
||||
mv dist/*-portable.zip artifact-portable/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload installer
|
||||
|
@@ -35,6 +35,10 @@
|
||||
|
||||
---
|
||||
|
||||
# Portable
|
||||
|
||||
For portable in windows, user can create folder `data` at the same directory as `Terminal.exe` to save the settings.
|
||||
|
||||
# Plugins
|
||||
|
||||
Plugins and themes can be installed directly from the Settings view inside Terminus.
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import * as electron from 'electron'
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
@@ -23,6 +24,7 @@ export class Application {
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache')
|
||||
app.commandLine.appendSwitch('lang', 'EN')
|
||||
app.allowRendererProcessReuse = false
|
||||
|
||||
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
|
||||
app.commandLine.appendSwitch(flag[0], flag[1])
|
||||
@@ -74,7 +76,7 @@ export class Application {
|
||||
this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
|
||||
}
|
||||
|
||||
this.tray.on('click', () => setTimeout(() => this.focus()));
|
||||
this.tray.on('click', () => setTimeout(() => this.focus()))
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([{
|
||||
label: 'Show',
|
||||
@@ -185,7 +187,7 @@ export class Application {
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||
|
@@ -20,25 +20,25 @@ export function parseArgs (argv, cwd) {
|
||||
return yargs.option('escape', {
|
||||
alias: 'e',
|
||||
type: 'boolean',
|
||||
describe: 'Perform shell escaping'
|
||||
describe: 'Perform shell escaping',
|
||||
}).positional('text', {
|
||||
type: 'string'
|
||||
type: 'string',
|
||||
})
|
||||
})
|
||||
.version('version', '', app.getVersion())
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
describe: 'Show DevTools on start',
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('hidden', {
|
||||
describe: 'Start minimized',
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('version', {
|
||||
alias: 'v',
|
||||
describe: 'Show version and exit',
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
})
|
||||
.help('help')
|
||||
.parse(argv.slice(1))
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import './portable'
|
||||
import './sentry'
|
||||
import './lru'
|
||||
import { app, ipcMain, Menu } from 'electron'
|
||||
import { parseArgs } from './cli'
|
||||
import { Application } from './app'
|
||||
import electronDebug = require('electron-debug')
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
if (!process.env.TERMINUS_PLUGINS) {
|
||||
process.env.TERMINUS_PLUGINS = ''
|
||||
@@ -13,14 +12,6 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
|
||||
const application = new Application()
|
||||
|
||||
if (process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
const portableData = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'terminus-data')
|
||||
if (!fs.existsSync(portableData)) {
|
||||
fs.mkdirSync(portableData)
|
||||
}
|
||||
app.setPath('userData', portableData)
|
||||
}
|
||||
|
||||
ipcMain.on('app:new-window', () => {
|
||||
application.newWindow()
|
||||
})
|
||||
@@ -68,8 +59,8 @@ app.on('ready', () => {
|
||||
label: 'New window',
|
||||
click () {
|
||||
this.app.newWindow()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]))
|
||||
}
|
||||
application.init()
|
||||
|
@@ -1,13 +1,15 @@
|
||||
let lru = require('lru-cache')({ max: 256, maxAge: 250 })
|
||||
|
||||
let fs = require('fs')
|
||||
let origLstat = fs.realpathSync.bind(fs)
|
||||
import * as createLRU from 'lru-cache'
|
||||
import * as fs from 'fs'
|
||||
const lru = createLRU({ max: 256, maxAge: 250 })
|
||||
const origLstat = fs.realpathSync.bind(fs)
|
||||
|
||||
// NB: The biggest offender of thrashing realpathSync is the node module system
|
||||
// itself, which we can't get into via any sane means.
|
||||
require('fs').realpathSync = function (p) {
|
||||
let r = lru.get(p)
|
||||
if (r) return r
|
||||
if (r) {
|
||||
return r
|
||||
}
|
||||
|
||||
r = origLstat(p)
|
||||
lru.set(p, r)
|
||||
|
24
app/lib/portable.ts
Executable file
24
app/lib/portable.ts
Executable file
@@ -0,0 +1,24 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
let appPath: string | null = null
|
||||
try {
|
||||
appPath = path.dirname(require('electron').app.getPath('exe'))
|
||||
} catch {
|
||||
appPath = path.dirname(require('electron').remote.app.getPath('exe'))
|
||||
}
|
||||
|
||||
if (null != appPath) {
|
||||
if(fs.existsSync(path.join(appPath, 'terminus-data'))) {
|
||||
fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data'))
|
||||
}
|
||||
const portableData = path.join(appPath, 'data')
|
||||
if (fs.existsSync(portableData)) {
|
||||
console.log('reset user data to ' + portableData)
|
||||
try {
|
||||
require('electron').app.setPath('userData', portableData)
|
||||
} catch {
|
||||
require('electron').remote.app.setPath('userData', portableData)
|
||||
}
|
||||
}
|
||||
}
|
0
app/lib/sentry.ts
Normal file → Executable file
0
app/lib/sentry.ts
Normal file → Executable file
@@ -27,6 +27,8 @@ export class Window {
|
||||
private windowConfig: ElectronConfig
|
||||
private windowBounds: Rectangle
|
||||
private closing = false
|
||||
private lastVibrancy: {enabled: boolean, type?: string} | null = null
|
||||
private disableVibrancyWhileDragging = false
|
||||
|
||||
get visible$ (): Observable<boolean> { return this.visible }
|
||||
|
||||
@@ -48,6 +50,7 @@ export class Window {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
preload: path.join(__dirname, 'sentry.js'),
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
frame: false,
|
||||
show: false,
|
||||
@@ -56,14 +59,14 @@ export class Window {
|
||||
|
||||
if (this.windowBounds) {
|
||||
Object.assign(bwOptions, this.windowBounds)
|
||||
const closestDisplay = screen.getDisplayNearestPoint( {x: this.windowBounds.x, y: this.windowBounds.y} )
|
||||
const closestDisplay = screen.getDisplayNearestPoint( { x: this.windowBounds.x, y: this.windowBounds.y } )
|
||||
|
||||
const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height];
|
||||
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height];
|
||||
const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height]
|
||||
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height]
|
||||
|
||||
if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) {
|
||||
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2;
|
||||
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2;
|
||||
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2
|
||||
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,11 +120,12 @@ export class Window {
|
||||
}
|
||||
|
||||
setVibrancy (enabled: boolean, type?: string) {
|
||||
this.lastVibrancy = { enabled, type }
|
||||
if (process.platform === 'win32') {
|
||||
if (parseFloat(os.release()) >= 10) {
|
||||
let attribValue = AccentState.ACCENT_DISABLED
|
||||
if (enabled) {
|
||||
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
|
||||
if (type === 'fluent') {
|
||||
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
} else {
|
||||
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
|
||||
@@ -131,6 +135,8 @@ export class Window {
|
||||
} else {
|
||||
DwmEnableBlurBehindWindow(this.window, enabled)
|
||||
}
|
||||
} else {
|
||||
this.window.setVibrancy(enabled ? 'dark' : null as any) // electron issue 20269
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +156,7 @@ export class Window {
|
||||
}
|
||||
|
||||
isDestroyed () {
|
||||
return !this.window || this.window.isDestroyed();
|
||||
return !this.window || this.window.isDestroyed()
|
||||
}
|
||||
|
||||
private setupWindowManagement () {
|
||||
@@ -293,6 +299,29 @@ export class Window {
|
||||
})
|
||||
|
||||
this.window.webContents.on('new-window', event => event.preventDefault())
|
||||
|
||||
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
||||
this.disableVibrancyWhileDragging = value
|
||||
})
|
||||
|
||||
this.window.on('will-move', () => {
|
||||
if (!this.lastVibrancy?.enabled || !this.disableVibrancyWhileDragging) {
|
||||
return
|
||||
}
|
||||
let timeout: number|null = null
|
||||
const oldVibrancy = this.lastVibrancy
|
||||
this.setVibrancy(false)
|
||||
const onMove = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
this.window.off('move', onMove)
|
||||
this.setVibrancy(oldVibrancy.enabled, oldVibrancy.type)
|
||||
}, 500)
|
||||
}
|
||||
this.window.on('move', onMove)
|
||||
})
|
||||
}
|
||||
|
||||
private destroy () {
|
||||
|
@@ -28,7 +28,7 @@
|
||||
"electron-updater": "^4.2.0",
|
||||
"fontmanager-redux": "0.4.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"keytar": "^5.0.0",
|
||||
"keytar": "^5.2.0",
|
||||
"mz": "^2.7.0",
|
||||
"ngx-toastr": "^10.2.0",
|
||||
"node-pty": "^0.10.0-beta2",
|
||||
@@ -49,6 +49,6 @@
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.32",
|
||||
"@types/node": "12.7.12",
|
||||
"node-abi": "^2.13.0"
|
||||
"node-abi": "^2.15.0"
|
||||
}
|
||||
}
|
||||
|
@@ -21,8 +21,7 @@ if (process.env.TERMINUS_DEV) {
|
||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const userPluginsPath = path.join(
|
||||
require('electron').remote.app.getPath('appData'),
|
||||
'terminus',
|
||||
require('electron').remote.app.getPath('userData'),
|
||||
'plugins',
|
||||
)
|
||||
|
||||
|
@@ -1485,10 +1485,10 @@ keyboardevents-areequal@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
||||
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
|
||||
|
||||
keytar@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.0.0.tgz#c89b6b7a4608fd7af633d9f8474b1a7eb97cbe6f"
|
||||
integrity sha512-a5UheK59YOlJf9i+2Osaj/kkH6mK0RCHVMtJ84u6ZfbfRIbOJ/H4b5VlOF/LgNHF6s78dRSBzZnvIuPiBKv6wg==
|
||||
keytar@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.2.0.tgz#7543190be98e2a751466096ce3ca1b1a4b8f39fe"
|
||||
integrity sha512-vsIX6n2BgTwzbKOSPIiJ8YduwHlPEE/G5dkmZWXaQK9qiGZMQyhxlFA4O6vrvM5fsXTMgUOrODYAqgpfNSRLDw==
|
||||
dependencies:
|
||||
nan "2.14.0"
|
||||
prebuild-install "5.3.3"
|
||||
@@ -1925,10 +1925,10 @@ ngx-toastr@^10.2.0:
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
node-abi@^2.13.0, node-abi@^2.7.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63"
|
||||
integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA==
|
||||
node-abi@^2.15.0, node-abi@^2.7.0:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.15.0.tgz#51d55cc711bd9e4a24a572ace13b9231945ccb10"
|
||||
integrity sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
|
60
electron-builder.yml
Normal file
60
electron-builder.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
appId: org.terminus
|
||||
productName: Terminus
|
||||
compression: normal
|
||||
afterSign: "./build/mac/afterSignHook.js"
|
||||
files:
|
||||
- "**/*"
|
||||
- dist
|
||||
extraResources:
|
||||
- builtin-plugins
|
||||
- extras
|
||||
publish:
|
||||
- provider: github
|
||||
|
||||
win:
|
||||
icon: "./build/windows/icon.ico"
|
||||
artifactName: terminus-${version}-portable.${ext}
|
||||
rfc3161TimeStampServer: http://sha256timestamp.ws.symantec.com/sha256/timestamp
|
||||
nsis:
|
||||
oneClick: false
|
||||
artifactName: terminus-${version}-setup.${ext}
|
||||
installerIcon: "./build/windows/icon.ico"
|
||||
|
||||
mac:
|
||||
category: public.app-category.video
|
||||
icon: "./build/mac/icon.icns"
|
||||
artifactName: terminus-${version}-macos.${ext}
|
||||
hardenedRuntime: true
|
||||
entitlements: "./build/mac/entitlements.plist"
|
||||
entitlementsInherit: "./build/mac/entitlements.plist"
|
||||
extendInfo:
|
||||
NSRequiresAquaSystemAppearance: false
|
||||
pkg:
|
||||
artifactName: terminus-${version}-macos.pkg
|
||||
|
||||
linux:
|
||||
category: Utilities
|
||||
icon: "./build/icons"
|
||||
artifactName: terminus-${version}-linux.${ext}
|
||||
executableArgs:
|
||||
- "--no-sandbox"
|
||||
snap:
|
||||
plugs:
|
||||
- default
|
||||
- system-files
|
||||
- system-observe
|
||||
deb:
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libnotify4
|
||||
- libsecret-1-0
|
||||
- libappindicator1
|
||||
- libxtst6
|
||||
- libnss3
|
||||
afterInstall: build/linux/after-install.tpl
|
||||
rpm:
|
||||
depends:
|
||||
- screen
|
||||
- gnome-python2-gnomekeyring
|
92
package.json
92
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"@fortawesome/fontawesome-free": "^5.12.1",
|
||||
"@sentry/cli": "^1.49.0",
|
||||
"@sentry/electron": "^1.0.0",
|
||||
"@types/electron-config": "^3.2.2",
|
||||
@@ -9,14 +9,14 @@
|
||||
"@types/node": "12.7.12",
|
||||
"@types/webpack-env": "1.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.13.0",
|
||||
"@typescript-eslint/parser": "^2.17.0",
|
||||
"@typescript-eslint/parser": "^2.19.2",
|
||||
"apply-loader": "2.0.0",
|
||||
"awesome-typescript-loader": "^5.0.0",
|
||||
"core-js": "^3.6.4",
|
||||
"cross-env": "6.0.3",
|
||||
"cross-env": "7.0.0",
|
||||
"css-loader": "3.4.2",
|
||||
"electron": "^7.1.10",
|
||||
"electron-builder": "22.1.0",
|
||||
"electron": "^8.0.0",
|
||||
"electron-builder": "22.3.2",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-installer-snap": "^5.0.0",
|
||||
"electron-notarize": "^0.1.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"graceful-fs": "^4.2.2",
|
||||
"html-loader": "0.5.5",
|
||||
"json-loader": "0.5.7",
|
||||
"node-abi": "^2.12.0",
|
||||
"node-abi": "^2.15.0",
|
||||
"node-gyp": "^6.1.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"npmlog": "4.1.2",
|
||||
@@ -42,12 +42,12 @@
|
||||
"shelljs": "0.8.3",
|
||||
"source-code-pro": "^2.30.2",
|
||||
"source-sans-pro": "3.6.0",
|
||||
"style-loader": "^1.1.2",
|
||||
"style-loader": "^1.1.3",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"to-string-loader": "1.1.6",
|
||||
"tslib": "^1.10.0",
|
||||
"typedoc": "^0.16.7",
|
||||
"typescript": "^3.7.4",
|
||||
"typescript": "^3.7.5",
|
||||
"url-loader": "^3.0.0",
|
||||
"val-loader": "2.1.0",
|
||||
"webpack": "^5.0.0-beta.12",
|
||||
@@ -55,79 +55,7 @@
|
||||
"yaml-loader": "0.5.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"*/node-abi": "^2.8.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.terminus",
|
||||
"productName": "Terminus",
|
||||
"compression": "normal",
|
||||
"afterSign": "./build/mac/afterSignHook.js",
|
||||
"files": [
|
||||
"**/*",
|
||||
"dist"
|
||||
],
|
||||
"extraResources": [
|
||||
"builtin-plugins",
|
||||
"extras"
|
||||
],
|
||||
"win": {
|
||||
"icon": "./build/windows/icon.ico",
|
||||
"artifactName": "terminus-${version}-setup.exe",
|
||||
"rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"artifactName": "terminus-${version}-setup.${ext}",
|
||||
"installerIcon": "./build/windows/icon.ico"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github"
|
||||
}
|
||||
],
|
||||
"portable": {
|
||||
"artifactName": "terminus-${version}-portable.exe"
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.video",
|
||||
"icon": "./build/mac/icon.icns",
|
||||
"artifactName": "terminus-${version}-macos.${ext}",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./build/mac/entitlements.plist",
|
||||
"entitlementsInherit": "./build/mac/entitlements.plist",
|
||||
"extendInfo": {
|
||||
"NSRequiresAquaSystemAppearance": false
|
||||
}
|
||||
},
|
||||
"pkg": {
|
||||
"artifactName": "terminus-${version}-macos.pkg"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utilities",
|
||||
"icon": "./build/icons",
|
||||
"artifactName": "terminus-${version}-linux.${ext}"
|
||||
},
|
||||
"snap": {
|
||||
"plugs": ["default", "system-files", "system-observe"]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"gconf2",
|
||||
"gconf-service",
|
||||
"libnotify4",
|
||||
"libsecret-1-0",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3"
|
||||
],
|
||||
"afterInstall": "build/linux/after-install.tpl"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"screen",
|
||||
"gnome-python2-gnomekeyring"
|
||||
]
|
||||
}
|
||||
"*/node-abi": "^2.14.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:typings && 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-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
|
||||
@@ -136,7 +64,7 @@
|
||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
||||
"prod": "cross-env TERMINUS_DEV=1 electron app",
|
||||
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src",
|
||||
"lint": "eslint --ext ts */src",
|
||||
"lint": "eslint --ext ts */src */lib",
|
||||
"postinstall": "node ./scripts/install-deps.js"
|
||||
},
|
||||
"repository": "eugeny/terminus"
|
||||
|
@@ -7,7 +7,7 @@ const isCI = !!process.env.GITHUB_REF
|
||||
|
||||
builder({
|
||||
dir: true,
|
||||
win: ['nsis', 'portable'],
|
||||
win: ['nsis', 'zip'],
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
|
@@ -19,3 +19,4 @@ export { HostAppService, Platform } from '../services/hostApp.service'
|
||||
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { TabsService } from '../services/tabs.service'
|
||||
export * from '../utils'
|
||||
|
@@ -107,6 +107,12 @@ export class AppRootComponent {
|
||||
if (hotkey === 'previous-tab') {
|
||||
this.app.previousTab()
|
||||
}
|
||||
if (hotkey === 'move-tab-left') {
|
||||
this.app.moveSelectedTabLeft()
|
||||
}
|
||||
if (hotkey === 'move-tab-right') {
|
||||
this.app.moveSelectedTabRight()
|
||||
}
|
||||
}
|
||||
if (hotkey === 'toggle-fullscreen') {
|
||||
this.hostApp.toggleFullscreen()
|
||||
@@ -184,6 +190,7 @@ export class AppRootComponent {
|
||||
if (this.config.store.appearance.dock === 'off') {
|
||||
// not docked, visible
|
||||
setTimeout(() => {
|
||||
this.hostApp.getWindow().show()
|
||||
this.hostApp.getWindow().focus()
|
||||
})
|
||||
} else {
|
||||
|
@@ -38,7 +38,7 @@ export abstract class BaseTabComponent {
|
||||
*/
|
||||
color: string|null = null
|
||||
|
||||
protected hasFocus = false
|
||||
hasFocus = false
|
||||
|
||||
/**
|
||||
* Ping this if your recovery state has been changed and you want
|
||||
|
@@ -564,6 +564,10 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
||||
}
|
||||
}
|
||||
}
|
||||
while (root.ratios.length < root.children.length) {
|
||||
root.ratios.push(1)
|
||||
}
|
||||
root.normalize()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,10 @@ hotkeys:
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-Left'
|
||||
- 'Ctrl-Shift-Tab'
|
||||
move-tab-left:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
|
@@ -14,6 +14,10 @@ hotkeys:
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-Tab'
|
||||
move-tab-left:
|
||||
- '⌘-Shift-Left'
|
||||
move-tab-right:
|
||||
- '⌘-Shift-Right'
|
||||
tab-1:
|
||||
- '⌘-1'
|
||||
tab-2:
|
||||
|
@@ -17,6 +17,10 @@ hotkeys:
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-Left'
|
||||
- 'Ctrl-Shift-Tab'
|
||||
move-tab-left:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
|
@@ -37,6 +37,14 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
id: 'previous-tab',
|
||||
name: 'Previous tab',
|
||||
},
|
||||
{
|
||||
id: 'move-tab-left',
|
||||
name: 'Move tab to the left',
|
||||
},
|
||||
{
|
||||
id: 'move-tab-right',
|
||||
name: 'Move tab to the right',
|
||||
},
|
||||
{
|
||||
id: 'tab-1',
|
||||
name: 'Tab 1',
|
||||
|
@@ -224,6 +224,35 @@ export class AppService {
|
||||
}
|
||||
}
|
||||
|
||||
moveSelectedTabLeft () {
|
||||
if (this.tabs.length > 1) {
|
||||
const tabIndex = this.tabs.indexOf(this._activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.swapTabs(this._activeTab, this.tabs[tabIndex - 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.swapTabs(this._activeTab, this.tabs[this.tabs.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveSelectedTabRight () {
|
||||
if (this.tabs.length > 1) {
|
||||
const tabIndex = this.tabs.indexOf(this._activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.swapTabs(this._activeTab, this.tabs[tabIndex + 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.swapTabs(this._activeTab, this.tabs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swapTabs (a: BaseTabComponent, b: BaseTabComponent) {
|
||||
const i1 = this.tabs.indexOf(a)
|
||||
const i2 = this.tabs.indexOf(b)
|
||||
this.tabs[i1] = b
|
||||
this.tabs[i2] = a
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabsChanged () {
|
||||
this.tabsChanged.next()
|
||||
|
@@ -4,6 +4,7 @@ import { Observable, Subject } from 'rxjs'
|
||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
|
||||
|
||||
export enum Platform {
|
||||
Linux, macOS, Windows,
|
||||
@@ -164,6 +165,14 @@ export class HostAppService {
|
||||
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
|
||||
this.configChangeBroadcast.next()
|
||||
}))
|
||||
|
||||
|
||||
if (
|
||||
isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED) &&
|
||||
!isWindowsBuild(WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED)
|
||||
) {
|
||||
electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,14 +228,12 @@ export class HostAppService {
|
||||
*
|
||||
* @param type `null`, or `fluent` when supported (Windowd only)
|
||||
*/
|
||||
setVibrancy (enable: boolean, type: string) {
|
||||
setVibrancy (enable: boolean, type: string|null) {
|
||||
if (!isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
|
||||
type = null
|
||||
}
|
||||
document.body.classList.toggle('vibrant', enable)
|
||||
if (this.platform === Platform.macOS) {
|
||||
this.getWindow().setVibrancy(enable ? 'dark' : null as any) // electron issue 20269
|
||||
}
|
||||
if (this.platform === Platform.Windows) {
|
||||
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
|
||||
}
|
||||
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
|
||||
}
|
||||
|
||||
setTitle (title: string) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||
import { stringifyKeySequence } from './hotkeys.util'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
@@ -20,8 +21,17 @@ interface EventBufferEntry {
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HotkeysService {
|
||||
key = new EventEmitter<KeyboardEvent>()
|
||||
|
||||
/** @hidden */
|
||||
matchedHotkey = new EventEmitter<string>()
|
||||
|
||||
/**
|
||||
* Fired for each recognized hotkey
|
||||
*/
|
||||
get hotkey$ (): Observable<string> { return this._hotkey }
|
||||
|
||||
globalHotkey = new EventEmitter<void>()
|
||||
private _hotkey = new Subject<string>()
|
||||
private currentKeystrokes: EventBufferEntry[] = []
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||
@@ -49,6 +59,9 @@ export class HotkeysService {
|
||||
this.getHotkeyDescriptions().then(hotkeys => {
|
||||
this.hotkeyDescriptions = hotkeys
|
||||
})
|
||||
|
||||
// deprecated
|
||||
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +84,7 @@ export class HotkeysService {
|
||||
const matched = this.getCurrentFullyMatchedHotkey()
|
||||
if (matched) {
|
||||
console.log('Matched hotkey', matched)
|
||||
this.matchedHotkey.emit(matched)
|
||||
this._hotkey.next(matched)
|
||||
this.clearCurrentKeystrokes()
|
||||
}
|
||||
})
|
||||
|
@@ -8,7 +8,7 @@ import { HostAppService, Platform } from './hostApp.service'
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch (_) { }
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
@@ -21,7 +21,18 @@ export class TabRecoveryService {
|
||||
window.localStorage.tabsRecovery = JSON.stringify(
|
||||
await Promise.all(
|
||||
tabs
|
||||
.map(tab => tab.getRecoveryToken())
|
||||
.map(tab => {
|
||||
let token = tab.getRecoveryToken()
|
||||
if (token) {
|
||||
token = token.then(r => {
|
||||
if (r) {
|
||||
r.tabColor = tab.color
|
||||
}
|
||||
return r
|
||||
})
|
||||
}
|
||||
return token
|
||||
})
|
||||
.filter(token => !!token)
|
||||
)
|
||||
)
|
||||
@@ -31,7 +42,9 @@ export class TabRecoveryService {
|
||||
for (const provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
||||
try {
|
||||
const tab = await provider.recover(token)
|
||||
if (tab) {
|
||||
if (tab !== null) {
|
||||
tab.options = tab.options || {}
|
||||
tab.options.color = token.tabColor || null
|
||||
return tab
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -8,6 +8,7 @@ import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { AppUpdater } from 'electron-updater'
|
||||
|
||||
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
||||
|
||||
@@ -18,12 +19,12 @@ export class UpdaterService {
|
||||
private downloaded: Promise<boolean>
|
||||
private electronUpdaterAvailable = true
|
||||
private updateURL: string
|
||||
private autoUpdater
|
||||
private autoUpdater: AppUpdater
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
private electron: ElectronService,
|
||||
config: ConfigService,
|
||||
private config: ConfigService,
|
||||
) {
|
||||
this.logger = log.create('updater')
|
||||
|
||||
@@ -48,9 +49,8 @@ export class UpdaterService {
|
||||
this.autoUpdater.once('update-downloaded', () => resolve(true))
|
||||
})
|
||||
|
||||
this.logger.debug('Checking for updates')
|
||||
|
||||
if (this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) {
|
||||
if (config.store.enableAutomaticUpdates && this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) {
|
||||
this.logger.debug('Checking for updates')
|
||||
try {
|
||||
this.autoUpdater.checkForUpdates()
|
||||
} catch (e) {
|
||||
@@ -61,6 +61,9 @@ export class UpdaterService {
|
||||
}
|
||||
|
||||
async check (): Promise<boolean> {
|
||||
if (!this.config.store.enableAutomaticUpdates) {
|
||||
return false
|
||||
}
|
||||
if (!this.electronUpdaterAvailable) {
|
||||
this.logger.debug('Checking for updates through fallback method.')
|
||||
const response = await axios.get(UPDATES_URL)
|
||||
|
@@ -3,6 +3,8 @@ import * as os from 'os'
|
||||
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
|
||||
export const WIN_BUILD_CONPTY_STABLE = 18309
|
||||
export const WIN_BUILD_WSL_EXE_DISTRO_FLAG = 17763
|
||||
export const WIN_BUILD_FLUENT_BG_SUPPORTED = 17063
|
||||
export const WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED = 18917
|
||||
|
||||
export function isWindowsBuild (build: number): boolean {
|
||||
return process.platform === 'win32' && parseFloat(os.release()) >= 10 && parseInt(os.release().split('.')[2]) >= build
|
@@ -46,9 +46,9 @@ async@^2.6.1:
|
||||
lodash "^4.17.11"
|
||||
|
||||
axios@^0.19.0:
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.1.tgz#8a6a04eed23dfe72747e1dd43c604b8f1677b5aa"
|
||||
integrity sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
|
||||
|
@@ -13,9 +13,9 @@ any-promise@^1.0.0:
|
||||
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
|
||||
|
||||
axios@^0.19.0:
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.1.tgz#8a6a04eed23dfe72747e1dd43c604b8f1677b5aa"
|
||||
integrity sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as os from 'os'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Component, Inject, Input, HostBinding, NgZone } from '@angular/core'
|
||||
import {
|
||||
@@ -14,6 +13,8 @@ import {
|
||||
Platform,
|
||||
HomeBaseService,
|
||||
ShellIntegrationService,
|
||||
isWindowsBuild,
|
||||
WIN_BUILD_FLUENT_BG_SUPPORTED,
|
||||
} from 'terminus-core'
|
||||
|
||||
import { SettingsTabProvider } from '../api'
|
||||
@@ -81,9 +82,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
this.hotkeyDescriptions = descriptions
|
||||
})
|
||||
|
||||
this.isFluentVibrancySupported = hostApp.platform === Platform.Windows
|
||||
&& parseFloat(os.release()) >= 10
|
||||
&& parseInt(os.release().split('.')[2]) >= 17063
|
||||
this.isFluentVibrancySupported = isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -99,10 +98,6 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return null
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.configSubscription.unsubscribe()
|
||||
this.config.save()
|
||||
|
@@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import TerminusCorePlugin, { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
|
||||
import TerminusCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
|
||||
|
||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||
@@ -11,7 +11,6 @@ import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
import { SettingsTabBodyComponent } from './components/settingsTabBody.component'
|
||||
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
import { SettingsHotkeyProvider } from './hotkeys'
|
||||
import { SettingsConfigProvider } from './config'
|
||||
|
||||
@@ -25,7 +24,6 @@ import { SettingsConfigProvider } from './config'
|
||||
],
|
||||
providers: [
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: SettingsConfigProvider, multi: true },
|
||||
{ provide: HotkeyProvider, useClass: SettingsHotkeyProvider, multi: true },
|
||||
],
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
||||
|
||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab|null> {
|
||||
if (recoveryToken && recoveryToken.type === 'app:settings') {
|
||||
return { type: SettingsTabComponent }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@@ -19,6 +19,8 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "12.7.3",
|
||||
"@types/ssh2": "^0.5.35",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"ssh2": "^0.8.2",
|
||||
"ssh2-streams": "^0.4.2",
|
||||
"sshpk": "^1.16.1",
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { BaseSession } from 'terminus-terminal'
|
||||
import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { Client, ClientChannel } from 'ssh2'
|
||||
@@ -32,6 +33,7 @@ export interface SSHConnection {
|
||||
readyTimeout?: number
|
||||
color?: string
|
||||
x11?: boolean
|
||||
skipBanner?: boolean
|
||||
|
||||
algorithms?: {[t: string]: string[]}
|
||||
}
|
||||
@@ -92,7 +94,7 @@ export class SSHSession extends BaseSession {
|
||||
try {
|
||||
this.shell = await this.openShellChannel({ x11: this.connection.x11 })
|
||||
} catch (err) {
|
||||
this.emitServiceMessage(`Remote rejected opening a shell channel: ${err}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`)
|
||||
}
|
||||
|
||||
this.shell.on('greeting', greeting => {
|
||||
@@ -159,13 +161,13 @@ export class SSHSession extends BaseSession {
|
||||
this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
|
||||
const forward = this.forwardedPorts.find(x => x.port === details.destPort)
|
||||
if (!forward) {
|
||||
this.emitServiceMessage(`Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
||||
return reject()
|
||||
}
|
||||
const socket = new Socket()
|
||||
socket.connect(forward.targetPort, forward.targetAddress)
|
||||
socket.on('error', e => {
|
||||
this.emitServiceMessage(`Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
||||
reject()
|
||||
})
|
||||
socket.on('connect', () => {
|
||||
@@ -195,7 +197,7 @@ export class SSHSession extends BaseSession {
|
||||
socket.connect(xPort, xHost)
|
||||
}
|
||||
socket.on('error', e => {
|
||||
this.emitServiceMessage(`Could not connect to the X server ${xHost}:${xPort}: ${e}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server ${xHost}:${xPort}: ${e}`)
|
||||
reject()
|
||||
})
|
||||
socket.on('connect', () => {
|
||||
@@ -231,7 +233,7 @@ export class SSHSession extends BaseSession {
|
||||
fw.targetPort,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
this.emitServiceMessage(`Remote has rejected the forwaded connection via ${fw}: ${err}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwaded connection via ${fw}: ${err}`)
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
@@ -246,10 +248,10 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
)
|
||||
}).then(() => {
|
||||
this.emitServiceMessage(`Forwaded ${fw}`)
|
||||
this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwaded ${fw}`)
|
||||
this.forwardedPorts.push(fw)
|
||||
}).catch(e => {
|
||||
this.emitServiceMessage(`Failed to forward port ${fw}: ${e}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
@@ -257,13 +259,13 @@ export class SSHSession extends BaseSession {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.ssh.forwardIn(fw.host, fw.port, err => {
|
||||
if (err) {
|
||||
this.emitServiceMessage(`Remote rejected port forwarding for ${fw}: ${err}`)
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
||||
return reject(err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
this.emitServiceMessage(`Forwaded ${fw}`)
|
||||
this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwaded ${fw}`)
|
||||
this.forwardedPorts.push(fw)
|
||||
}
|
||||
}
|
||||
|
@@ -85,6 +85,11 @@
|
||||
placeholder='#000000'
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Skip MoTD/banner
|
||||
toggle([(ngModel)]='connection.skipBanner')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Keep Alive Interval (Milliseconds)
|
||||
|
@@ -82,10 +82,11 @@ export class EditConnectionModalComponent {
|
||||
this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: this.connection.privateKey,
|
||||
title: 'Select private key',
|
||||
}
|
||||
).then(result => {
|
||||
if (result.filePaths) {
|
||||
if (!result.canceled) {
|
||||
this.connection.privateKey = result.filePaths[0]
|
||||
}
|
||||
})
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { SSHConnection, SSHConnectionGroup } from '../api'
|
||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||
import { PromptModalComponent } from './promptModal.component'
|
||||
@@ -19,6 +20,7 @@ export class SSHSettingsTabComponent {
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private passwordStorage: PasswordStorageService,
|
||||
) {
|
||||
this.connections = this.config.store.ssh.connections
|
||||
this.refresh()
|
||||
@@ -65,6 +67,7 @@ export class SSHSettingsTabComponent {
|
||||
}
|
||||
)).response === 1) {
|
||||
this.connections = this.connections.filter(x => x !== connection)
|
||||
this.passwordStorage.deletePassword(connection)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
this.refresh()
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { Spinner } from 'cli-spinner'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs/operators'
|
||||
@@ -43,23 +45,32 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
this.session = this.ssh.createSession(this.connection)
|
||||
this.session.serviceMessage$.subscribe(msg => {
|
||||
this.write(`\r\n[SSH] ${msg}\r\n`)
|
||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ' ' + msg + '\r\n')
|
||||
this.session.resize(this.size.columns, this.size.rows)
|
||||
})
|
||||
this.attachSessionHandlers()
|
||||
this.write(`Connecting to ${this.connection.host}`)
|
||||
const interval = setInterval(() => this.write('.'), 500)
|
||||
|
||||
const spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
stream: {
|
||||
write: x => this.write(x),
|
||||
},
|
||||
})
|
||||
spinner.setSpinnerString(6)
|
||||
spinner.start()
|
||||
|
||||
try {
|
||||
await this.ssh.connectSession(this.session, (message: string) => {
|
||||
this.write('\r\n' + message)
|
||||
spinner.stop(true)
|
||||
this.write(message + '\r\n')
|
||||
spinner.start()
|
||||
})
|
||||
spinner.stop(true)
|
||||
} catch (e) {
|
||||
this.write('\r\n')
|
||||
this.write(e.message)
|
||||
spinner.stop(true)
|
||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||
return
|
||||
} finally {
|
||||
clearInterval(interval)
|
||||
this.write('\r\n')
|
||||
}
|
||||
await this.session.start()
|
||||
this.session.resize(this.size.columns, this.size.rows)
|
||||
@@ -69,6 +80,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
return {
|
||||
type: 'app:ssh-tab',
|
||||
connection: this.connection,
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,10 @@ export class RecoveryProvider extends TabRecoveryProvider {
|
||||
if (recoveryToken && recoveryToken.type === 'app:ssh-tab') {
|
||||
return {
|
||||
type: SSHTabComponent,
|
||||
options: { connection: recoveryToken.connection },
|
||||
options: {
|
||||
connection: recoveryToken.connection,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
@@ -13,7 +14,7 @@ import { PasswordStorageService } from './passwordStorage.service'
|
||||
import { SSH2Stream } from 'ssh2-streams'
|
||||
|
||||
try {
|
||||
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -65,17 +66,17 @@ export class SSHService {
|
||||
if (!privateKeyPath) {
|
||||
const userKeyPath = path.join(process.env.HOME as string, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
log(`Using user's default private key: ${userKeyPath}`)
|
||||
log('Using user\'s default private key')
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyPath) {
|
||||
log(`Loading private key from ${privateKeyPath}`)
|
||||
log('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||
try {
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
log('Could not read the private key file')
|
||||
log(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
||||
this.toastr.error('Could not read the private key file')
|
||||
}
|
||||
|
||||
@@ -86,7 +87,7 @@ export class SSHService {
|
||||
} catch (e) {
|
||||
if (e instanceof sshpk.KeyEncryptedError) {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
log('Key requires passphrase')
|
||||
log(colors.bgYellow.yellow.black(' ! ') + ' Key requires passphrase')
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
let passphrase = ''
|
||||
@@ -104,7 +105,7 @@ export class SSHService {
|
||||
}
|
||||
}
|
||||
|
||||
privateKey = parsedKey!.toString('ssh')
|
||||
privateKey = parsedKey!.toString('pem')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ export class SSHService {
|
||||
})
|
||||
})
|
||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||
log(`Keyboard-interactive auth requested: ${name}`)
|
||||
log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`)
|
||||
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
||||
const results: string[] = []
|
||||
for (const prompt of prompts) {
|
||||
@@ -147,11 +148,15 @@ export class SSHService {
|
||||
}))
|
||||
|
||||
ssh.on('greeting', greeting => {
|
||||
log('Greeting: ' + greeting)
|
||||
if (!session.connection.skipBanner) {
|
||||
log('Greeting: ' + greeting)
|
||||
}
|
||||
})
|
||||
|
||||
ssh.on('banner', banner => {
|
||||
log('Banner: \n' + banner)
|
||||
if (!session.connection.skipBanner) {
|
||||
log(banner)
|
||||
}
|
||||
})
|
||||
|
||||
let agent: string|null = null
|
||||
@@ -182,7 +187,8 @@ export class SSHService {
|
||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||
readyTimeout: session.connection.readyTimeout,
|
||||
hostVerifier: digest => {
|
||||
log('SHA256 fingerprint: ' + digest)
|
||||
log(colors.bgWhite(' ') + ' Host key fingerprint:')
|
||||
log(colors.bgWhite(' ') + ' ' + colors.black.bgWhite(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||
return true
|
||||
},
|
||||
hostHash: 'sha256' as any,
|
||||
|
@@ -20,13 +20,18 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ssh2@^0.5.35":
|
||||
version "0.5.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.39.tgz#e39ab7e38fc01337f66237ed6c42237ef3e58e3b"
|
||||
integrity sha512-MtX4tr6Jtdn/JPVsQytjB/NBeOd7JfCyf/l79KOdkUYL+r4GXUXcySX1n8W4O61fnQdljTBXEPJJ2dhnGhi2xg==
|
||||
version "0.5.40"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.40.tgz#b65465897f40c67e66e630b1f059f13a1ea7f1fa"
|
||||
integrity sha512-Yrs2vFIirdscR+xY4leiUosHTClRbVBiFLlm5gA0JMZMjbCulHKO3qcgMqATElrquiZal5sSHoXMk4t8DzDawA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/ssh2-streams" "*"
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
|
||||
|
||||
asn1@~0.2.0, asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
@@ -46,6 +51,11 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
cli-spinner@^0.2.10:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"
|
||||
integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
|
@@ -19,6 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@types/deep-equal": "^1.0.0",
|
||||
"@types/slug": "^0.9.1",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"dataurl": "0.1.0",
|
||||
"deep-equal": "1.1.0",
|
||||
"hterm-umdjs": "1.4.1",
|
||||
@@ -30,8 +31,10 @@
|
||||
"xterm": "^4.4.0-beta.15",
|
||||
"xterm-addon-fit": "^0.4.0-beta2",
|
||||
"xterm-addon-ligatures": "^0.2.1",
|
||||
"xterm-addon-search": "^0.4.0",
|
||||
"xterm-addon-webgl": "^0.5.0-beta.7",
|
||||
"xterm-addon-search": "^0.5.0",
|
||||
"xterm-addon-serialize": "^0.1.1",
|
||||
"xterm-addon-unicode11": "^0.1.1",
|
||||
"xterm-addon-webgl": "^0.6.0-beta.2",
|
||||
"zmodem.js": "^0.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import colors from 'ansi-colors'
|
||||
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input, ElementRef } from '@angular/core'
|
||||
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger, TabContextMenuItemProvider } from 'terminus-core'
|
||||
@@ -34,6 +35,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
])]
|
||||
|
||||
session: BaseSession
|
||||
savedState: any
|
||||
|
||||
@Input() zoom = 0
|
||||
|
||||
@Input() showSearchPanel = false
|
||||
@@ -171,6 +174,14 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.size = { columns, rows }
|
||||
this.frontendReady.next()
|
||||
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
try {
|
||||
decorator.attach(this)
|
||||
} catch (e) {
|
||||
this.logger.warn('Decorator attach() throws', e)
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(columns, rows)
|
||||
}, 1000)
|
||||
@@ -178,6 +189,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
|
||||
if (this.savedState) {
|
||||
this.frontend.restoreState(this.savedState)
|
||||
this.frontend.write('\r\n\r\n')
|
||||
this.frontend.write(colors.bgWhite.black(' * ') + colors.bgBlackBright.white(' History restored '))
|
||||
this.frontend.write('\r\n\r\n')
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
if (this.hasFocus) {
|
||||
this.frontend.attach(this.content.nativeElement)
|
||||
@@ -194,14 +212,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
|
||||
this.configure()
|
||||
|
||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||
try {
|
||||
decorator.attach(this)
|
||||
} catch (e) {
|
||||
this.logger.warn('Decorator attach() throws', e)
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.output.subscribe(() => {
|
||||
this.displayActivity()
|
||||
|
@@ -1,16 +1,35 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||
|
||||
/**
|
||||
* Extend to automatically run actions on new terminals
|
||||
*/
|
||||
export abstract class TerminalDecorator {
|
||||
private smartSubscriptions = new Map<BaseTerminalTabComponent, Subscription[]>()
|
||||
|
||||
/**
|
||||
* Called when a new terminal tab starts
|
||||
*/
|
||||
attach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Called before a terminal tab is destroyed
|
||||
* Called before a terminal tab is destroyed.
|
||||
* Make sure to call super()
|
||||
*/
|
||||
detach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line
|
||||
detach (terminal: BaseTerminalTabComponent): void {
|
||||
for (const s of this.smartSubscriptions.get(terminal) || []) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
this.smartSubscriptions.delete(terminal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically cancel @subscription once detached from @terminal
|
||||
*/
|
||||
protected subscribeUntilDetached (terminal: BaseTerminalTabComponent, subscription: Subscription) {
|
||||
if (!this.smartSubscriptions.has(terminal)) {
|
||||
this.smartSubscriptions.set(terminal, [])
|
||||
}
|
||||
this.smartSubscriptions.get(terminal)?.push(subscription)
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,9 @@ import deepEqual from 'deep-equal'
|
||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
|
||||
import { ConfigService, HostAppService, Platform, ElectronService, getCSSFontFamily } from 'terminus-core'
|
||||
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
|
||||
import { TerminalColorScheme } from '../api/interfaces'
|
||||
import { getCSSFontFamily } from '../utils'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
|
@@ -2,11 +2,10 @@ import slug from 'slug'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ConfigService, ElectronService, HostAppService, Platform } from 'terminus-core'
|
||||
import { ConfigService, ElectronService, HostAppService, Platform, WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from 'terminus-core'
|
||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||
import { Shell, Profile } from '../api/interfaces'
|
||||
import { TerminalService } from '../services/terminal.service'
|
||||
import { WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from '../utils'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { BaseTabProcess } from 'terminus-core'
|
||||
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
|
||||
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
||||
import { SessionOptions } from '../api/interfaces'
|
||||
import { Session } from '../services/sessions.service'
|
||||
import { WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from '../utils'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
@@ -65,6 +64,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
...this.sessionOptions,
|
||||
cwd: cwd || this.sessionOptions.cwd,
|
||||
},
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
}
|
||||
|
||||
|
69
terminus-terminal/src/features/debug.ts
Normal file
69
terminus-terminal/src/features/debug.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as fs from 'fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from '../api/decorator'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
import { ElectronService, HostAppService } from 'terminus-core'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class DebugDecorator extends TerminalDecorator {
|
||||
constructor (
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
terminal.content.nativeElement.addEventListener('keyup', e => {
|
||||
if (e.which === 49 && e.ctrlKey && e.shiftKey && e.altKey) {
|
||||
this.doSaveOutput(terminal)
|
||||
}
|
||||
if (e.which === 50 && e.ctrlKey && e.shiftKey && e.altKey) {
|
||||
this.doLoadInput(terminal)
|
||||
}
|
||||
if (e.which === 51 && e.ctrlKey && e.shiftKey && e.altKey) {
|
||||
this.doCopyOutput(terminal)
|
||||
}
|
||||
if (e.which === 52 && e.ctrlKey && e.shiftKey && e.altKey) {
|
||||
this.doPasteOutput(terminal)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async doSaveOutput (terminal: TerminalTabComponent) {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: 'output.txt',
|
||||
},
|
||||
)
|
||||
if (result.filePath) {
|
||||
fs.writeFileSync(result.filePath, terminal.frontend.saveState())
|
||||
}
|
||||
}
|
||||
|
||||
async doCopyOutput (terminal: TerminalTabComponent) {
|
||||
const data = '```' + JSON.stringify(terminal.frontend.saveState()) + '```'
|
||||
this.electron.clipboard.writeText(data)
|
||||
}
|
||||
|
||||
async doLoadInput (terminal: TerminalTabComponent) {
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
buttonLabel: 'Load',
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
},
|
||||
)
|
||||
if (result.filePaths.length) {
|
||||
const data = fs.readFileSync(result.filePaths[0])
|
||||
terminal.frontend.restoreState(data)
|
||||
}
|
||||
}
|
||||
|
||||
async doPasteOutput (terminal: TerminalTabComponent) {
|
||||
const data = JSON.parse(this.electron.clipboard.readText())
|
||||
terminal.frontend.restoreState(data)
|
||||
}
|
||||
}
|
29
terminus-terminal/src/features/pathDrop.ts
Normal file
29
terminus-terminal/src/features/pathDrop.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from '../api/decorator'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class PathDropDecorator extends TerminalDecorator {
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
setTimeout(() => {
|
||||
this.subscribeUntilDetached(terminal, terminal.frontend.dragOver$.subscribe(event => {
|
||||
event.preventDefault()
|
||||
}))
|
||||
this.subscribeUntilDetached(terminal, terminal.frontend.drop$.subscribe(event => {
|
||||
for (const file of event.dataTransfer!.files as any) {
|
||||
this.injectPath(terminal, file.path)
|
||||
}
|
||||
event.preventDefault()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
injectPath (terminal: TerminalTabComponent, path: string) {
|
||||
if (path.includes(' ')) {
|
||||
path = `"${path}"`
|
||||
}
|
||||
path = path.replace(/\\/g, '\\\\')
|
||||
terminal.sendInput(path + ' ')
|
||||
}
|
||||
}
|
@@ -1,29 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import colors from 'ansi-colors'
|
||||
import * as ZModem from 'zmodem.js'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from './api/decorator'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { LogService, Logger, ElectronService, HostAppService } from 'terminus-core'
|
||||
import { TerminalDecorator } from '../api/decorator'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
import { LogService, Logger, ElectronService, HostAppService, HotkeysService } from 'terminus-core'
|
||||
|
||||
const SPACER = ' '
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ZModemDecorator extends TerminalDecorator {
|
||||
private subscriptions: Subscription[] = []
|
||||
private logger: Logger
|
||||
private activeSession: any = null
|
||||
private cancelEvent: Observable<any>
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
hotkeys: HotkeysService,
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('zmodem')
|
||||
this.cancelEvent = hotkeys.hotkey$.pipe(filter(x => x === 'ctrl-c'))
|
||||
}
|
||||
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
@@ -47,27 +51,27 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
},
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.subscriptions = [
|
||||
terminal.session.binaryOutput$.subscribe(data => {
|
||||
const chunkSize = 1024
|
||||
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
|
||||
try {
|
||||
sentry.consume(data.subarray(i * chunkSize, (i + 1) * chunkSize))
|
||||
} catch (e) {
|
||||
this.logger.error('protocol error', e)
|
||||
this.activeSession.abort()
|
||||
this.activeSession = null
|
||||
terminal.enablePassthrough = true
|
||||
return
|
||||
}
|
||||
this.subscribeUntilDetached(terminal, terminal.session.binaryOutput$.subscribe(data => {
|
||||
const chunkSize = 1024
|
||||
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
|
||||
try {
|
||||
sentry.consume(data.subarray(i * chunkSize, (i + 1) * chunkSize))
|
||||
} catch (e) {
|
||||
this.logger.error('protocol error', e)
|
||||
this.activeSession.abort()
|
||||
this.activeSession = null
|
||||
terminal.enablePassthrough = true
|
||||
return
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async process (terminal, detection) {
|
||||
this.showMessage(terminal, '[Terminus] ZModem session started')
|
||||
this.showMessage(terminal, colors.bgBlue.black(' ZMODEM ') + ' Session started')
|
||||
this.showMessage(terminal, '------------------------')
|
||||
|
||||
const zsession = detection.confirm()
|
||||
this.activeSession = zsession
|
||||
this.logger.info('new session', zsession)
|
||||
@@ -94,7 +98,7 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
await zsession.close()
|
||||
} else {
|
||||
zsession.on('offer', xfer => {
|
||||
this.receiveFile(terminal, xfer)
|
||||
this.receiveFile(terminal, xfer, zsession)
|
||||
})
|
||||
|
||||
zsession.start()
|
||||
@@ -104,15 +108,12 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
}
|
||||
}
|
||||
|
||||
detach (_terminal: TerminalTabComponent): void {
|
||||
for (const s of this.subscriptions) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async receiveFile (terminal, xfer) {
|
||||
const details = xfer.get_details()
|
||||
this.showMessage(terminal, `🟡 Offered ${details.name}`, true)
|
||||
private async receiveFile (terminal, xfer, zsession) {
|
||||
const details: {
|
||||
name: string,
|
||||
size: number,
|
||||
} = xfer.get_details()
|
||||
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + details.name, true)
|
||||
this.logger.info('offered', xfer)
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostApp.getWindow(),
|
||||
@@ -121,20 +122,48 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
},
|
||||
)
|
||||
if (!result.filePath) {
|
||||
this.showMessage(terminal, `🔴 Rejected ${details.name}`)
|
||||
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + details.name)
|
||||
xfer.skip()
|
||||
return
|
||||
}
|
||||
|
||||
const stream = fs.createWriteStream(result.filePath)
|
||||
let bytesSent = 0
|
||||
await xfer.accept({
|
||||
on_input: chunk => {
|
||||
stream.write(Buffer.from(chunk))
|
||||
bytesSent += chunk.length
|
||||
this.showMessage(terminal, `🟡 Receiving ${details.name}: ${Math.round(100 * bytesSent / details.size)}%`, true)
|
||||
},
|
||||
let canceled = false
|
||||
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
||||
if (terminal.hasFocus) {
|
||||
try {
|
||||
zsession._skip()
|
||||
} catch {}
|
||||
canceled = true
|
||||
}
|
||||
})
|
||||
this.showMessage(terminal, `✅ Received ${details.name}`)
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
xfer.accept({
|
||||
on_input: chunk => {
|
||||
if (canceled) {
|
||||
return
|
||||
}
|
||||
stream.write(Buffer.from(chunk))
|
||||
bytesSent += chunk.length
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true)
|
||||
},
|
||||
}),
|
||||
this.cancelEvent.toPromise(),
|
||||
])
|
||||
|
||||
if (canceled) {
|
||||
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name)
|
||||
} else {
|
||||
this.showMessage(terminal, colors.bgGreen.black(' Received ') + ' ' + details.name)
|
||||
}
|
||||
} catch {
|
||||
this.showMessage(terminal, colors.bgRed.black(' Error ') + ' ' + details.name)
|
||||
}
|
||||
|
||||
cancelSubscription.unsubscribe()
|
||||
stream.end()
|
||||
}
|
||||
|
||||
@@ -149,23 +178,46 @@ export class ZModemDecorator extends TerminalDecorator {
|
||||
bytes_remaining: stat.size,
|
||||
}
|
||||
this.logger.info('offering', offer)
|
||||
this.showMessage(terminal, `🟡 Offering ${offer.name}`, true)
|
||||
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
|
||||
|
||||
const xfer = await zsession.send_offer(offer)
|
||||
if (xfer) {
|
||||
let bytesSent = 0
|
||||
let canceled = false
|
||||
const stream = fs.createReadStream(filePath)
|
||||
const cancelSubscription = this.cancelEvent.subscribe(() => {
|
||||
if (terminal.hasFocus) {
|
||||
canceled = true
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (canceled) {
|
||||
stream.close()
|
||||
return
|
||||
}
|
||||
xfer.send(chunk)
|
||||
bytesSent += chunk.length
|
||||
this.showMessage(terminal, `🟡 Sending ${offer.name}: ${Math.round(100 * bytesSent / offer.size)}%`, true)
|
||||
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
|
||||
})
|
||||
await new Promise(resolve => stream.on('end', resolve))
|
||||
|
||||
await Promise.race([
|
||||
new Promise(resolve => stream.on('end', resolve)),
|
||||
this.cancelEvent.toPromise(),
|
||||
])
|
||||
|
||||
await xfer.end()
|
||||
|
||||
if (canceled) {
|
||||
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + offer.name)
|
||||
} else {
|
||||
this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
|
||||
}
|
||||
|
||||
stream.close()
|
||||
this.showMessage(terminal, `✅ Sent ${offer.name}`)
|
||||
cancelSubscription.unsubscribe()
|
||||
} else {
|
||||
this.showMessage(terminal, `🔴 Other side rejected ${offer.name}`)
|
||||
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)
|
||||
this.logger.warn('rejected by the other side')
|
||||
}
|
||||
}
|
@@ -74,4 +74,7 @@ export abstract class Frontend {
|
||||
|
||||
abstract findNext (term: string, searchOptions?: SearchOptions): boolean
|
||||
abstract findPrevious (term: string, searchOptions?: SearchOptions): boolean
|
||||
|
||||
abstract saveState (): any
|
||||
abstract restoreState (state: any): void
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Frontend, SearchOptions } from './frontend'
|
||||
import { hterm, preferenceManager } from './hterm'
|
||||
import { getCSSFontFamily } from '../utils'
|
||||
import { getCSSFontFamily } from 'terminus-core'
|
||||
|
||||
/** @hidden */
|
||||
export class HTermFrontend extends Frontend {
|
||||
@@ -164,6 +164,12 @@ export class HTermFrontend extends Frontend {
|
||||
return false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
saveState (): any { }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
restoreState (_state: any): void { }
|
||||
|
||||
private setFontSize () {
|
||||
const size = this.configuredFontSize * Math.pow(1.1, this.zoom)
|
||||
preferenceManager.set('font-size', size)
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { getCSSFontFamily } from 'terminus-core'
|
||||
import { Frontend, SearchOptions } from './frontend'
|
||||
import { Terminal, ITheme } from 'xterm'
|
||||
import { getCSSFontFamily } from '../utils'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import { LigaturesAddon } from 'xterm-addon-ligatures'
|
||||
import { SearchAddon } from 'xterm-addon-search'
|
||||
import { WebglAddon } from 'xterm-addon-webgl'
|
||||
import { Unicode11Addon } from 'xterm-addon-unicode11'
|
||||
import { SerializeAddon } from 'xterm-addon-serialize'
|
||||
import './xterm.css'
|
||||
import deepEqual from 'deep-equal'
|
||||
import { Attributes } from 'xterm/src/common/buffer/Constants'
|
||||
@@ -30,6 +32,7 @@ export class XTermFrontend extends Frontend {
|
||||
private copyOnSelect = false
|
||||
private search = new SearchAddon()
|
||||
private fitAddon = new FitAddon()
|
||||
private serializeAddon = new SerializeAddon()
|
||||
private ligaturesAddon: LigaturesAddon
|
||||
private opened = false
|
||||
|
||||
@@ -57,7 +60,11 @@ export class XTermFrontend extends Frontend {
|
||||
this.copySelection()
|
||||
}
|
||||
})
|
||||
|
||||
this.xterm.loadAddon(this.fitAddon)
|
||||
this.xterm.loadAddon(this.serializeAddon)
|
||||
this.xterm.loadAddon(new Unicode11Addon())
|
||||
this.xterm.unicode.activeVersion = '11'
|
||||
|
||||
const keyboardEventHandler = (name: string, event: KeyboardEvent) => {
|
||||
this.hotkeysService.pushKeystroke(name, event)
|
||||
@@ -248,6 +255,14 @@ export class XTermFrontend extends Frontend {
|
||||
return this.search.findPrevious(term, searchOptions)
|
||||
}
|
||||
|
||||
saveState (): any {
|
||||
return this.serializeAddon.serialize(1000)
|
||||
}
|
||||
|
||||
restoreState (state: any): void {
|
||||
this.xterm.write(state)
|
||||
}
|
||||
|
||||
private setFontSize () {
|
||||
const scale = Math.pow(1.1, this.zoom)
|
||||
this.xterm.setOption('fontSize', this.configuredFontSize * scale)
|
||||
|
@@ -31,12 +31,13 @@ import { TerminalContextMenuItemProvider } from './api/contextMenuProvider'
|
||||
import { TerminalColorSchemeProvider } from './api/colorSchemeProvider'
|
||||
import { ShellProvider } from './api/shellProvider'
|
||||
import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ShellSettingsTabProvider } from './settings'
|
||||
import { PathDropDecorator } from './pathDrop'
|
||||
import { DebugDecorator } from './features/debug'
|
||||
import { PathDropDecorator } from './features/pathDrop'
|
||||
import { ZModemDecorator } from './features/zmodem'
|
||||
import { TerminalConfigProvider } from './config'
|
||||
import { TerminalHotkeyProvider } from './hotkeys'
|
||||
import { HyperColorSchemes } from './colorSchemes'
|
||||
import { NewTabContextMenu, CopyPasteContextMenu, SaveAsProfileContextMenu, LegacyContextMenu } from './tabContextMenu'
|
||||
import { ZModemDecorator } from './zmodem'
|
||||
|
||||
import { CmderShellProvider } from './shells/cmder'
|
||||
import { CustomShellProvider } from './shells/custom'
|
||||
@@ -77,6 +78,7 @@ import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend'
|
||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: ZModemDecorator, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: DebugDecorator, multi: true },
|
||||
|
||||
{ provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
|
||||
|
@@ -1,40 +0,0 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from './api/decorator'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class PathDropDecorator extends TerminalDecorator {
|
||||
private subscriptions: Subscription[] = []
|
||||
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
setTimeout(() => {
|
||||
this.subscriptions = [
|
||||
terminal.frontend.dragOver$.subscribe(event => {
|
||||
event.preventDefault()
|
||||
}),
|
||||
terminal.frontend.drop$.subscribe(event => {
|
||||
for (const file of event.dataTransfer!.files as any) {
|
||||
this.injectPath(terminal, file.path)
|
||||
}
|
||||
event.preventDefault()
|
||||
}),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
injectPath (terminal: TerminalTabComponent, path: string) {
|
||||
if (path.includes(' ')) {
|
||||
path = `"${path}"`
|
||||
}
|
||||
path = path.replace(/\\/g, '\\\\')
|
||||
terminal.sendInput(path + ' ')
|
||||
}
|
||||
|
||||
detach (_terminal: TerminalTabComponent): void {
|
||||
for (const s of this.subscriptions) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,7 +10,10 @@ export class RecoveryProvider extends TabRecoveryProvider {
|
||||
if (recoveryToken && recoveryToken.type === 'app:terminal-tab') {
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
options: { sessionOptions: recoveryToken.sessionOptions },
|
||||
options: {
|
||||
sessionOptions: recoveryToken.sessionOptions,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
@@ -6,19 +6,18 @@ import * as nodePTY from 'node-pty'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService, ConfigService } from 'terminus-core'
|
||||
import { Logger, LogService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
|
||||
import { exec } from 'mz/child_process'
|
||||
import { SessionOptions } from '../api/interfaces'
|
||||
import { WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from '../utils'
|
||||
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ElectronService } from 'terminus-core'
|
||||
import { ElectronService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
|
||||
import { SessionOptions } from '../api/interfaces'
|
||||
|
||||
import { WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from '../utils'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UACService {
|
||||
|
@@ -8,7 +8,7 @@ import { Shell } from '../api/interfaces'
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
/** @hidden */
|
||||
|
@@ -8,7 +8,7 @@ import { Shell } from '../api/interfaces'
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
/** @hidden */
|
||||
|
@@ -8,7 +8,7 @@ import { Shell } from '../api/interfaces'
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
/** @hidden */
|
||||
|
@@ -6,7 +6,7 @@ import { Shell } from '../api/interfaces'
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
/** @hidden */
|
||||
|
@@ -2,30 +2,32 @@ import * as fs from 'mz/fs'
|
||||
import slug from 'slug'
|
||||
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
import { HostAppService, Platform, isWindowsBuild, WIN_BUILD_WSL_EXE_DISTRO_FLAG } from 'terminus-core'
|
||||
|
||||
import { ShellProvider } from '../api/shellProvider'
|
||||
import { Shell } from '../api/interfaces'
|
||||
import { isWindowsBuild, WIN_BUILD_WSL_EXE_DISTRO_FLAG } from '../utils'
|
||||
|
||||
/* eslint-disable block-scoped-var */
|
||||
|
||||
try {
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||
} catch { }
|
||||
|
||||
// WSL Distribution List
|
||||
// https://docs.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice
|
||||
var wslIconMap: { [key: string]: string } = {
|
||||
'Alpine': 'alpine.svg',
|
||||
'Debian': 'debian.svg',
|
||||
'kali-linux': 'linux.svg',
|
||||
'SLES-12': 'suse.svg',
|
||||
'openSUSE-Leap-15-1': 'suse.svg',
|
||||
'Ubuntu-18.04': 'ubuntu.svg',
|
||||
'Ubuntu': 'ubuntu.svg',
|
||||
'Linux': 'linux.svg',
|
||||
/* eslint-disable quote-props */
|
||||
const wslIconMap: { [key: string]: string } = {
|
||||
'Alpine': require('../icons/alpine.svg'),
|
||||
'Debian': require('../icons/debian.svg'),
|
||||
'kali-linux': require('../icons/linux.svg'),
|
||||
'SLES-12': require('../icons/suse.svg'),
|
||||
'openSUSE-Leap-15-1': require('../icons/suse.svg'),
|
||||
'Ubuntu-16.04': require('../icons/ubuntu.svg'),
|
||||
'Ubuntu-18.04': require('../icons/ubuntu.svg'),
|
||||
'Ubuntu': require('../icons/ubuntu.svg'),
|
||||
'Linux': require('../icons/linux.svg'),
|
||||
}
|
||||
/* eslint-enable quote-props */
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -50,7 +52,7 @@ export class WSLShellProvider extends ShellProvider {
|
||||
|
||||
if (null != lxss && null != lxss.DefaultDistribution) {
|
||||
const defaultDistKey = wnr.getRegistryKey(wnr.HK.CU, lxssPath + '\\' + String(lxss.DefaultDistribution.value))
|
||||
if (defaultDistKey.DistributionName) {
|
||||
if (defaultDistKey?.DistributionName) {
|
||||
const shell: Shell = {
|
||||
id: 'wsl',
|
||||
name: 'WSL / Default distro',
|
||||
@@ -59,9 +61,7 @@ export class WSLShellProvider extends ShellProvider {
|
||||
TERM: 'xterm-color',
|
||||
COLORTERM: 'truecolor',
|
||||
},
|
||||
}
|
||||
if (wslIconMap.hasOwnProperty(defaultDistKey.DistributionName.value)) {
|
||||
shell['icon'] = require(`../icons/${wslIconMap[defaultDistKey.DistributionName.value]}`)
|
||||
icon: wslIconMap[defaultDistKey.DistributionName.value],
|
||||
}
|
||||
shells.push(shell)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class WSLShellProvider extends ShellProvider {
|
||||
return [{
|
||||
id: 'wsl',
|
||||
name: 'WSL / Bash on Windows',
|
||||
icon: require(`../icons/${wslIconMap['linux']}`),
|
||||
icon: wslIconMap['Linux'],
|
||||
command: bashPath,
|
||||
env: {
|
||||
TERM: 'xterm-color',
|
||||
@@ -99,9 +99,7 @@ export class WSLShellProvider extends ShellProvider {
|
||||
TERM: 'xterm-color',
|
||||
COLORTERM: 'truecolor',
|
||||
},
|
||||
}
|
||||
if (wslIconMap.hasOwnProperty(name)) {
|
||||
shell['icon'] = require(`../icons/${wslIconMap[name]}`)
|
||||
icon: wslIconMap[name],
|
||||
}
|
||||
shells.push(shell)
|
||||
}
|
||||
|
@@ -170,6 +170,9 @@ export class LegacyContextMenu extends TabContextMenuItemProvider {
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
if (!this.contextMenuProviders) {
|
||||
return []
|
||||
}
|
||||
if (tab instanceof BaseTerminalTabComponent) {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (const p of this.contextMenuProviders) {
|
||||
|
@@ -12,6 +12,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/slug/-/slug-0.9.1.tgz#16dbf8b77d73e0a09ce51a96400878f33806ab32"
|
||||
integrity sha512-zR/u8WFQ4/6uCIikjI00a5uB084XjgEGNRAvM4a1BL39Bw9yEiDQFiPS2DgJ8lPDkR2Qd/vZ26dCR9XqlKbDqQ==
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
|
||||
|
||||
any-promise@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
|
||||
@@ -244,20 +249,30 @@ xterm-addon-ligatures@^0.2.1:
|
||||
font-finder "^1.0.4"
|
||||
font-ligatures "^1.3.2"
|
||||
|
||||
xterm-addon-search@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.4.0.tgz#a7beadb3caa7330eb31fb1f17d92de25537684a1"
|
||||
integrity sha512-g07qb/Z4aSfrQ25e6Z6rz6KiExm2DvesQXkx+eA715VABBr5VM/9Jf0INoCiDSYy/nn7rpna+kXiGVJejIffKg==
|
||||
xterm-addon-search@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.5.0.tgz#cd3a2f8056084c28e236d4e732da37682010bcc2"
|
||||
integrity sha512-zLVqVTrg5w2nk9fRj3UuVKCPo/dmFe/cLf3EM9Is5Dm6cgOoXmeo9eq2KgD8A0gquAflTFTf0ya2NaFmShHwyg==
|
||||
|
||||
xterm-addon-webgl@^0.5.0-beta.7:
|
||||
version "0.5.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67"
|
||||
integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA==
|
||||
xterm-addon-serialize@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.1.2.tgz#339a38520bf90cf51f447cb74f3ae0e530d5ee39"
|
||||
integrity sha512-etSTiN+I3lqWefMixiiVo1V4lPjmfInFmwtjbYDYWyUwPU5tveEZZSu15b52Q26IPaJdl8RxBgNTYu6Q3YauLQ==
|
||||
|
||||
xterm-addon-unicode11@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.1.1.tgz#b209ef137db38096f68636af4ef4d0c0acba85ad"
|
||||
integrity sha512-z6vJTL+dpNljwAYzYoyDjJP8A2XjZuEosl0sRa+FGRf3jEyEVWquDM53MfUd1ztVdAPQ839qR6eYK1BXV04Bhw==
|
||||
|
||||
xterm-addon-webgl@^0.6.0-beta.2:
|
||||
version "0.6.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.6.0-beta.2.tgz#00cb4e17db0bfd4de59c4d1ce9a2d1b533111c81"
|
||||
integrity sha512-vFDk8/YkzSITGxWPxtfCGqrlY89YD/UZwZHl5lxL5B9gLlzueGgZxQfKEIUcFSqMPxBnTOf9aueGYBDRap3AbA==
|
||||
|
||||
xterm@^4.4.0-beta.15:
|
||||
version "4.4.0-beta.15"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc"
|
||||
integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ==
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589"
|
||||
integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig==
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
|
Reference in New Issue
Block a user