mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-31 14:36:57 +00:00
Compare commits
57 Commits
v1.0.0-alp
...
v1.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2bea4b9d6c | ||
![]() |
4a76c12f15 | ||
![]() |
181f3e3d33 | ||
![]() |
ee2fadbf60 | ||
![]() |
4259d3b53d | ||
![]() |
65aaa131ef | ||
![]() |
46d9aabbdd | ||
![]() |
692045ce77 | ||
![]() |
9c257b0002 | ||
![]() |
15c23eb7dd | ||
![]() |
5fc67d3648 | ||
![]() |
571884f39c | ||
![]() |
ccbcd30813 | ||
![]() |
30666c2838 | ||
![]() |
953558a866 | ||
![]() |
ace81aced2 | ||
![]() |
dc781deeb0 | ||
![]() |
e24d3d56eb | ||
![]() |
6f35e60468 | ||
![]() |
110b57bc64 | ||
![]() |
fd47a32bdb | ||
![]() |
dfd1ffbffc | ||
![]() |
f841cfeb5e | ||
![]() |
9d2398bc12 | ||
![]() |
da9cee0792 | ||
![]() |
aaac14dbd5 | ||
![]() |
23396b5c53 | ||
![]() |
021390952a | ||
![]() |
ad59baa4f5 | ||
![]() |
0420b2dbb9 | ||
![]() |
fab9429707 | ||
![]() |
5b62d5f92a | ||
![]() |
298637a150 | ||
![]() |
49c738451e | ||
![]() |
892b18df4d | ||
![]() |
de6e545f8f | ||
![]() |
e3d1d5e61e | ||
![]() |
d9e337aa46 | ||
![]() |
2881481fc2 | ||
![]() |
fa4c59e3c0 | ||
![]() |
f783e1ab06 | ||
![]() |
5cdb7527c8 | ||
![]() |
13a76db9af | ||
![]() |
0de12b6b38 | ||
![]() |
92993db122 | ||
![]() |
02082c385c | ||
![]() |
0c15f5033d | ||
![]() |
a280658bbb | ||
![]() |
3673542197 | ||
![]() |
11f188f1e8 | ||
![]() |
9a9db28054 | ||
![]() |
47d57d08ee | ||
![]() |
39e2c386f0 | ||
![]() |
c73d39026b | ||
![]() |
89dff969b1 | ||
![]() |
e1eb1beb87 | ||
![]() |
8d12d6a547 |
@@ -14,12 +14,12 @@ cache:
|
||||
- app/node_modules
|
||||
|
||||
before_install:
|
||||
- npm i
|
||||
- yarn install
|
||||
- scripts/install-deps.js
|
||||
|
||||
script:
|
||||
- scripts/build-native.js
|
||||
- npm run build
|
||||
- yarn run build
|
||||
- scripts/prepackage-plugins.js
|
||||
- scripts/build-$BUILD_FOR.js
|
||||
|
||||
@@ -32,3 +32,7 @@ addons:
|
||||
- rpm
|
||||
- wine
|
||||
- mono-runtime
|
||||
- yarn
|
||||
sources:
|
||||
- sourceline: 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||
key_url: 'https://dl.yarnpkg.com/debian/pubkey.gpg'
|
||||
|
@@ -10,14 +10,13 @@ First, install the dependencies:
|
||||
|
||||
```
|
||||
# macOS/Linux:
|
||||
sudo npm -g install yarn node-gyp
|
||||
yarn install
|
||||
npm install
|
||||
./scripts/install-deps.js
|
||||
./scripts/build-native.js
|
||||
|
||||
# Windows:
|
||||
npm -g install yarn node-gyp windows-build-tools
|
||||
yarn install
|
||||
npm -g install windows-build-tools
|
||||
npm install
|
||||
node scripts\install-deps.js
|
||||
node scripts\build-native.js
|
||||
```
|
||||
|
@@ -27,6 +27,7 @@
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"js-yaml": "3.8.2",
|
||||
"mz": "^2.6.0",
|
||||
"ngx-toastr": "^8.0.0",
|
||||
"path": "0.12.7",
|
||||
"rxjs": "5.3.0",
|
||||
"zone.js": "0.8.12"
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
export function getRootModule (plugins: any[]) {
|
||||
let imports = [
|
||||
BrowserModule,
|
||||
...(plugins.map(x => x.default.forRoot ? x.default.forRoot() : x.default)),
|
||||
...plugins,
|
||||
NgbModule.forRoot(),
|
||||
ToastrModule.forRoot({
|
||||
positionClass: 'toast-bottom-center',
|
||||
preventDuplicates: true,
|
||||
extendedTimeOut: 5000,
|
||||
}),
|
||||
]
|
||||
let bootstrap = [
|
||||
...(plugins.filter(x => x.bootstrap).map(x => x.bootstrap)),
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'source-sans-pro'
|
||||
import 'font-awesome/css/font-awesome.css'
|
||||
import 'ngx-toastr/toastr.css'
|
||||
import './preload.scss'
|
||||
|
||||
import * as Raven from 'raven-js'
|
||||
|
@@ -30,6 +30,7 @@ async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgM
|
||||
(document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%'
|
||||
})
|
||||
let module = getRootModule(pluginsModules)
|
||||
window['rootModule'] = module
|
||||
return await platformBrowserDynamic().bootstrapModule(module)
|
||||
}
|
||||
|
||||
|
@@ -59,6 +59,7 @@ const builtinModules = [
|
||||
'@angular/platform-browser',
|
||||
'@angular/platform-browser-dynamic',
|
||||
'@ng-bootstrap/ng-bootstrap',
|
||||
'ngx-toastr',
|
||||
'rxjs',
|
||||
'terminus-core',
|
||||
'terminus-settings',
|
||||
@@ -82,7 +83,7 @@ nodeRequire('module').prototype.require = function (query) {
|
||||
export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
let paths = nodeModule.globalPaths
|
||||
let foundPlugins: IPluginInfo[] = []
|
||||
let candidateLocations: { pluginDir: string, pluginName: string }[] = []
|
||||
let candidateLocations: { pluginDir: string, packageName: string }[] = []
|
||||
|
||||
for (let pluginDir of paths) {
|
||||
pluginDir = normalizePath(pluginDir)
|
||||
@@ -93,36 +94,38 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
if (await fs.exists(path.join(pluginDir, 'package.json'))) {
|
||||
candidateLocations.push({
|
||||
pluginDir: path.dirname(pluginDir),
|
||||
pluginName: path.basename(pluginDir)
|
||||
packageName: path.basename(pluginDir)
|
||||
})
|
||||
}
|
||||
for (let pluginName of pluginNames) {
|
||||
candidateLocations.push({ pluginDir, pluginName })
|
||||
for (let packageName of pluginNames) {
|
||||
candidateLocations.push({ pluginDir, packageName })
|
||||
}
|
||||
}
|
||||
|
||||
for (let { pluginDir, pluginName } of candidateLocations) {
|
||||
let pluginPath = path.join(pluginDir, pluginName)
|
||||
for (let { pluginDir, packageName } of candidateLocations) {
|
||||
let pluginPath = path.join(pluginDir, packageName)
|
||||
let infoPath = path.join(pluginPath, 'package.json')
|
||||
if (!await fs.exists(infoPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (foundPlugins.some(x => x.name === pluginName.substring('terminus-'.length))) {
|
||||
console.info(`Plugin ${pluginName} already exists, overriding`)
|
||||
foundPlugins = foundPlugins.filter(x => x.name !== pluginName.substring('terminus-'.length))
|
||||
let name = packageName.substring('terminus-'.length)
|
||||
|
||||
if (foundPlugins.some(x => x.name === name)) {
|
||||
console.info(`Plugin ${packageName} already exists, overriding`)
|
||||
foundPlugins = foundPlugins.filter(x => x.name !== name)
|
||||
}
|
||||
|
||||
try {
|
||||
let info = JSON.parse(await fs.readFile(infoPath, {encoding: 'utf-8'}))
|
||||
if (!info.keywords || info.keywords.indexOf('terminus-plugin') === -1) {
|
||||
if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
|
||||
continue
|
||||
}
|
||||
let author = info.author
|
||||
author = author.name || author
|
||||
foundPlugins.push({
|
||||
name: pluginName.substring('terminus-'.length),
|
||||
packageName: pluginName,
|
||||
name: name,
|
||||
packageName: packageName,
|
||||
isBuiltin: pluginDir === builtinPluginsPath,
|
||||
version: info.version,
|
||||
description: info.description,
|
||||
@@ -131,7 +134,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
info,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Cannot load package info for', pluginName)
|
||||
console.error('Cannot load package info for', packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +150,10 @@ export async function loadPlugins (foundPlugins: IPluginInfo[], progress: Progre
|
||||
console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
|
||||
progress(index, foundPlugins.length)
|
||||
try {
|
||||
let pluginModule = nodeRequire(foundPlugin.path)
|
||||
let packageModule = nodeRequire(foundPlugin.path)
|
||||
let pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
|
||||
pluginModule['pluginName'] = foundPlugin.name
|
||||
pluginModule['bootstrap'] = packageModule.bootstrap
|
||||
plugins.push(pluginModule)
|
||||
} catch (error) {
|
||||
console.error(`Could not load ${foundPlugin.name}:`, error)
|
||||
|
@@ -58,3 +58,7 @@
|
||||
color: #842fe0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
@@ -58,6 +58,7 @@ module.exports = {
|
||||
'child_process': 'commonjs child_process',
|
||||
'electron': 'commonjs electron',
|
||||
'electron-is-dev': 'commonjs electron-is-dev',
|
||||
'ngx-toastr': 'commonjs ngx-toastr',
|
||||
'module': 'commonjs module',
|
||||
'mz': 'commonjs mz',
|
||||
'path': 'commonjs path',
|
||||
|
@@ -195,6 +195,10 @@ mz@^2.6.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
ngx-toastr@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-8.0.0.tgz#f3bc53146b2f7da3eabf3daa1b1bbdf65cb49697"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en_US</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ELEMENTS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.syslink.elements</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Elements Client</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>elements-client</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>619</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.8.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016 Syslink GmbH. All rights reserved.</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>AtomApplication</string>
|
||||
</dict>
|
||||
</plist>
|
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix RequiredVersion="3.6.2830.0" xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
|
||||
<Bundle Name="ELEMENTS" Version="$(var.Version)" Manufacturer="ELEMENTS.tv" UpgradeCode="508475fc-0e76-4cd1-8e98-6953023ba518"
|
||||
HelpUrl="http://elements.tv"
|
||||
Copyright="Copyright © 2016 ELEMENTS" IconSourceFile="build/icon.ico"
|
||||
AboutUrl="http://elements.tv">
|
||||
|
||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.HyperlinkLicense">
|
||||
<bal:WixStandardBootstrapperApplication
|
||||
LicenseUrl=""
|
||||
LogoFile="build\logo.png"
|
||||
ThemeFile="build\windows\wix-theme.xml"
|
||||
/>
|
||||
</BootstrapperApplicationRef>
|
||||
|
||||
<Chain>
|
||||
<MsiPackage
|
||||
Id="ClientMSI"
|
||||
Compressed="yes"
|
||||
ForcePerMachine="yes"
|
||||
SourceFile="dist\elements-app.msi"
|
||||
Vital="yes">
|
||||
</MsiPackage>
|
||||
<MsiPackage
|
||||
Id="DriverMSI"
|
||||
Compressed="yes"
|
||||
ForcePerMachine="yes"
|
||||
SourceFile="build/windows/ElementsDriver_x64.msi"
|
||||
Vital="yes">
|
||||
</MsiPackage>
|
||||
</Chain>
|
||||
</Bundle>
|
||||
</Wix>
|
@@ -1,84 +0,0 @@
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*" UpgradeCode="37484543-5276-2386-5427-275941245342"
|
||||
Name="ELEMENTS" Version="$(var.ProductVersion)" Manufacturer="ELEMENTS.tv" Language="1033">
|
||||
<Package InstallerVersion="200"
|
||||
Compressed="yes"
|
||||
Comments="Windows Installer Package"
|
||||
Platform="x64"
|
||||
InstallScope="perMachine"
|
||||
InstallPrivileges="elevated" />
|
||||
<MajorUpgrade AllowDowngrades="yes" Schedule="afterInstallValidate" />
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>
|
||||
|
||||
<Feature Id="DefaultFeature" Level="1">
|
||||
<ComponentRef Id="RegistryEntries" />
|
||||
<ComponentRef Id="AppDir"/>
|
||||
<ComponentRef Id="AvidSharedStorageAccess"/>
|
||||
<ComponentRef Id="ApplicationShortcutDesktop"/>
|
||||
<ComponentGroupRef Id="Files" />
|
||||
</Feature>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="DesktopFolder" Name="Desktop">
|
||||
<Component Id="ApplicationShortcutDesktop" Guid="*">
|
||||
<Shortcut Id="ApplicationDesktopShortcut"
|
||||
Name="ELEMENTS"
|
||||
Description="ELEMENTS client app"
|
||||
Target="[INSTALLDIR]\\Elements.exe"
|
||||
WorkingDirectory="INSTALLDIR"/>
|
||||
<RemoveFolder Id="DesktopFolder" On="uninstall"/>
|
||||
<RegistryValue
|
||||
Root="HKCU"
|
||||
Key="Software\ELEMENTS"
|
||||
Name="installed"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="ElementsDir" Name="ELEMENTS">
|
||||
<Directory Id="INSTALLDIR" Name="ELEMENTS Client">
|
||||
<Component Id="AppDir" Guid="284957a6-a462-4e34-babd-c17800f11054" Win64="yes">
|
||||
<CreateFolder />
|
||||
<!--RemoveFile Id="RemoveFilesFromAppDirectory" Name="*.*" On="uninstall" /-->
|
||||
<!--RemoveFolder Id="AppDir" On="uninstall"/-->
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Component Id="RegistryEntries" Guid="572998d8-719e-4124-8fe6-6d4f8b855d7b">
|
||||
<RegistryKey Root="HKLM"
|
||||
Key="system\currentcontrolset\services\AvidFs"
|
||||
Action="create">
|
||||
<RegistryValue Type="string" Name="Description" Value="AIFMRX" />
|
||||
<RegistryValue Type="string" Name="DisplayName" Value="AIFMRX" />
|
||||
<RegistryValue Type="integer" Name="ErrorControl" Value="1" />
|
||||
<RegistryValue Type="string" Name="Group" Value="Network" />
|
||||
<RegistryValue Type="string" Name="ImagePath" Value="System32\DRIVERS\aifmrx.sys" />
|
||||
<RegistryValue Type="integer" Name="Start" Value="1" />
|
||||
<RegistryValue Type="integer" Name="Type" Value="2" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKLM"
|
||||
Key="system\currentcontrolset\services\AifMRx\NetworkProvider"
|
||||
Action="create">
|
||||
<RegistryValue Type="string" Name="DeviceName" Value="\Device\AvidFs" />
|
||||
<RegistryValue Type="string" Name="Name" Value="Interface Network" />
|
||||
<RegistryValue Type="string" Name="ProviderPath" Value="System32\aifmrxnp.dll" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKLM"
|
||||
Key="system\CurrentControlSet\services\LanmanWorkstation\Parameters"
|
||||
Action="create">
|
||||
<RegistryValue Type="integer" Name="DisableLargeMtu" Value="0" KeyPath="yes" />
|
||||
<RegistryValue Type="integer" Name="DisableBandwidthThrottling" Value="1" />
|
||||
<RegistryValue Type="integer" Name="EnableWsd" Value="0" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<Directory Id="System64Folder" Name="SystemFolder">
|
||||
<Component Id="AvidSharedStorageAccess" Guid="972c67f2-ee17-4b20-8939-b92cfa13fcf6" NeverOverwrite="yes" Win64="yes" Permanent="yes">
|
||||
<File Id="AvidSharedStorageAccess.dll" Source="build\windows\AvidSharedStorageAccess.dll" KeyPath="yes"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Product>
|
||||
</Wix>
|
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||
|
||||
|
||||
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
|
||||
<Window Width="300" Height="360" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
|
||||
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
|
||||
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
|
||||
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
|
||||
|
||||
<Image X="30" Y="30" Width="256" Height="60" ImageFile="logo.png" Visible="yes"/>
|
||||
|
||||
<Page Name="Help">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
|
||||
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
|
||||
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Install">
|
||||
<!--Hypertext Name="EulaHyperlink" X="11" Y="121" Width="-11" Height="51" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
||||
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox-->
|
||||
<Button Name="InstallButton" X="90" Y="-120" Width="120" Height="50" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
|
||||
<Button Name="WelcomeCancelButton" X="110" Y="-80" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Options">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
|
||||
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
|
||||
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
|
||||
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="FilesInUse">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
|
||||
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
|
||||
|
||||
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
|
||||
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
|
||||
|
||||
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
|
||||
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Progress">
|
||||
<Text X="30" Y="120" Width="-30" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
|
||||
<Text X="30" Y="150" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
|
||||
<Text Name="OverallProgressPackageText" X="30" Y="200" Width="-30" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
|
||||
<Progressbar Name="OverallCalculatedProgressbar" X="30" Y="220" Width="-30" Height="20" />
|
||||
<Button Name="ProgressCancelButton" X="110" Y="-40" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Modify">
|
||||
<Text X="30" Y="110" Width="-30" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
|
||||
|
||||
<Button Name="UninstallButton" X="90" Y="-100" Width="120" Height="50" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
|
||||
<Button Name="RepairButton" X="110" Y="-60" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.ModifyRepairButton)</Button>
|
||||
<Button Name="ModifyCancelButton" X="110" Y="-30" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Success">
|
||||
<Text Name="SuccessHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Success</Text>
|
||||
<Text Name="SuccessInstallHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Installed successfully</Text>
|
||||
<Text Name="SuccessRepairHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Repaired successfully</Text>
|
||||
<Text Name="SuccessUninstallHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Uninstalled successfully</Text>
|
||||
|
||||
<Button Name="LaunchButton" X="90" Y="-100" Width="120" Height="50" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||
<Button Name="SuccessCancelButton" X="110" Y="-60" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Failure">
|
||||
<Text Name="FailureHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
|
||||
<Text Name="FailureInstallHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Setup failed</Text>
|
||||
<Text Name="FailureUninstallHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Uninstall failed</Text>
|
||||
<Text Name="FailureRepairHeader" X="30" Y="110" Width="-30" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">Repair failed</Text>
|
||||
<Hypertext Name="FailureLogFileLink" X="30" Y="145" Width="-30" Height="50" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||
<Hypertext Name="FailureMessageText" X="30" Y="195" Width="-30" Height="50" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||
<Text Name="FailureRestartText" X="-30" Y="255" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
|
||||
|
||||
<Button Name="FailureRestartButton" X="90" Y="-100" Width="120" Height="50" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||
<Button Name="FailureCloseButton" X="110" Y="-60" Width="80" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
|
||||
</Page>
|
||||
</Theme>
|
20
package.json
20
package.json
@@ -15,6 +15,7 @@
|
||||
"electron-rebuild": "1.5.11",
|
||||
"file-loader": "0.9.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
"html-loader": "0.4.4",
|
||||
"json-loader": "0.5.4",
|
||||
"less": "2.7.1",
|
||||
@@ -23,6 +24,7 @@
|
||||
"node-gyp": "^3.6.2",
|
||||
"node-sass": "^4.5.3",
|
||||
"npmlog": "4.1.0",
|
||||
"npx": "^9.7.1",
|
||||
"pug": "2.0.0-beta11",
|
||||
"pug-html-loader": "1.0.9",
|
||||
"pug-loader": "2.3.0",
|
||||
@@ -41,7 +43,8 @@
|
||||
"url-loader": "0.5.7",
|
||||
"val-loader": "0.5.0",
|
||||
"webpack": "^3.0.0",
|
||||
"yaml-loader": "0.4.0"
|
||||
"yaml-loader": "0.4.0",
|
||||
"yarn": "^1.3.2"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.terminus",
|
||||
@@ -53,7 +56,9 @@
|
||||
],
|
||||
"win": {
|
||||
"icon": "./build/windows/icon.ico",
|
||||
"publish": ["github"]
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
|
||||
@@ -63,7 +68,9 @@
|
||||
"category": "public.app-category.video",
|
||||
"icon": "./build/mac/icon.icns",
|
||||
"identity": null,
|
||||
"publish": ["github"]
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"dmg": {
|
||||
"artifactName": "terminus-${version}-${os}-${arch}.dmg"
|
||||
@@ -71,7 +78,9 @@
|
||||
"linux": {
|
||||
"category": "Utilities",
|
||||
"icon": "./build/icons",
|
||||
"publish": ["github"]
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
@@ -82,6 +91,7 @@
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3",
|
||||
"python-gnomekeyring",
|
||||
"tmux"
|
||||
],
|
||||
"artifactName": "terminus-${version}-${os}-${arch}.deb"
|
||||
@@ -94,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "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",
|
||||
"build": "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": "webpack --progress --color --watch",
|
||||
"start": "cross-env DEV=1 electron --js-flags='--ignition' app --debug",
|
||||
"prod": "cross-env DEV=1 electron --js-flags='--ignition' app",
|
||||
|
@@ -3,7 +3,5 @@ const rebuild = require('electron-rebuild').default
|
||||
const path = require('path')
|
||||
const vars = require('./vars')
|
||||
|
||||
let buildPath = path.resolve(__dirname, '../terminus-terminal')
|
||||
rebuild(buildPath, vars.electronVersion, process.arch, [], true).then(() => {
|
||||
console.log('Done')
|
||||
})
|
||||
rebuild(path.resolve(__dirname, '../terminus-ssh'), vars.electronVersion, process.arch, [], true)
|
||||
rebuild(path.resolve(__dirname, '../terminus-terminal'), vars.electronVersion, process.arch, [], true)
|
||||
|
@@ -4,16 +4,19 @@ const path = require('path')
|
||||
const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
|
||||
const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
|
||||
const npx = `${localBinPath}/npx`;
|
||||
|
||||
log.info('deps', 'app')
|
||||
sh.exec('yarn install')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
|
||||
sh.cd('app')
|
||||
sh.exec('yarn install')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
sh.cd('..')
|
||||
|
||||
vars.builtinPlugins.forEach(plugin => {
|
||||
log.info('deps', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec('yarn install')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
sh.cd('..')
|
||||
})
|
||||
|
@@ -14,6 +14,7 @@ exports.builtinPlugins = [
|
||||
'terminus-terminal',
|
||||
'terminus-community-color-schemes',
|
||||
'terminus-plugin-manager',
|
||||
'terminus-ssh',
|
||||
]
|
||||
exports.nativeModules = ['node-pty-tmp', 'font-manager']
|
||||
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
|
||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
|
@@ -1,44 +1,54 @@
|
||||
!
|
||||
! Generated with :
|
||||
! XRDB2Xreources.py
|
||||
!
|
||||
*.foreground: #d8d8d8
|
||||
*.background: #181818
|
||||
*.cursorColor: #d8d8d8
|
||||
!
|
||||
! Black
|
||||
*.color0: #181818
|
||||
*.color8: #585858
|
||||
!
|
||||
! Red
|
||||
*.color1: #ab4642
|
||||
*.color9: #ab4642
|
||||
!
|
||||
! Green
|
||||
*.color2: #a1b56c
|
||||
*.color10: #a1b56c
|
||||
!
|
||||
! Yellow
|
||||
*.color3: #f7ca88
|
||||
*.color11: #f7ca88
|
||||
!
|
||||
! Blue
|
||||
*.color4: #7cafc2
|
||||
*.color12: #7cafc2
|
||||
!
|
||||
! Magenta
|
||||
*.color5: #ba8baf
|
||||
*.color13: #ba8baf
|
||||
!
|
||||
! Cyan
|
||||
*.color6: #86c1b9
|
||||
*.color14: #86c1b9
|
||||
!
|
||||
! White
|
||||
*.color7: #d8d8d8
|
||||
*.color15: #f8f8f8
|
||||
!
|
||||
! Bold, Italic, Underline
|
||||
*.colorBD: #d8d8d8
|
||||
!*.colorIT:
|
||||
!*.colorUL:
|
||||
! Base16 Default Dark
|
||||
! Scheme: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
#define base00 #181818
|
||||
#define base01 #282828
|
||||
#define base02 #383838
|
||||
#define base03 #585858
|
||||
#define base04 #b8b8b8
|
||||
#define base05 #d8d8d8
|
||||
#define base06 #e8e8e8
|
||||
#define base07 #f8f8f8
|
||||
#define base08 #ab4642
|
||||
#define base09 #dc9656
|
||||
#define base0A #f7ca88
|
||||
#define base0B #a1b56c
|
||||
#define base0C #86c1b9
|
||||
#define base0D #7cafc2
|
||||
#define base0E #ba8baf
|
||||
#define base0F #a16946
|
||||
|
||||
*.foreground: base05
|
||||
#ifdef background_opacity
|
||||
*.background: [background_opacity]base00
|
||||
#else
|
||||
*.background: base00
|
||||
#endif
|
||||
*.cursorColor: base05
|
||||
|
||||
*.color0: base00
|
||||
*.color1: base08
|
||||
*.color2: base0B
|
||||
*.color3: base0A
|
||||
*.color4: base0D
|
||||
*.color5: base0E
|
||||
*.color6: base0C
|
||||
*.color7: base05
|
||||
|
||||
*.color8: base03
|
||||
*.color9: base08
|
||||
*.color10: base0B
|
||||
*.color11: base0A
|
||||
*.color12: base0D
|
||||
*.color13: base0E
|
||||
*.color14: base0C
|
||||
*.color15: base07
|
||||
|
||||
! Note: colors beyond 15 might not be loaded (e.g., xterm, urxvt),
|
||||
! use 'shell' template to set these if necessary
|
||||
*.color16: base09
|
||||
*.color17: base0F
|
||||
*.color18: base01
|
||||
*.color19: base02
|
||||
*.color20: base04
|
||||
*.color21: base06
|
||||
|
@@ -10,38 +10,38 @@ export class ColorSchemes extends TerminalColorSchemeProvider {
|
||||
|
||||
schemeContents.keys().forEach(schemeFile => {
|
||||
let lines = (schemeContents(schemeFile) as string).split('\n')
|
||||
|
||||
// process #define variables
|
||||
let variables: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('#define'))
|
||||
.map(x => x.split(' ').map(v => v.trim()))
|
||||
.forEach(([ignore, variableName, variableValue]) => {
|
||||
variables[variableName] = variableValue
|
||||
})
|
||||
|
||||
let values: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('*.'))
|
||||
.map(x => x.substring(2))
|
||||
.map(x => x.split(':').map(v => v.trim()))
|
||||
.forEach(([key, value]) => {
|
||||
values[key] = value
|
||||
values[key] = variables[value] ? variables[value] : value
|
||||
})
|
||||
|
||||
let colors: string[] = []
|
||||
let colorIndex = 0
|
||||
while (values[`color${colorIndex}`]) {
|
||||
colors.push(values[`color${colorIndex}`])
|
||||
colorIndex++
|
||||
}
|
||||
|
||||
schemes.push({
|
||||
name: schemeFile.split('/')[1].trim(),
|
||||
foreground: values.foreground,
|
||||
background: values.background,
|
||||
cursor: values.cursorColor,
|
||||
colors: [
|
||||
values.color0,
|
||||
values.color1,
|
||||
values.color2,
|
||||
values.color3,
|
||||
values.color4,
|
||||
values.color5,
|
||||
values.color6,
|
||||
values.color7,
|
||||
values.color8,
|
||||
values.color9,
|
||||
values.color10,
|
||||
values.color11,
|
||||
values.color12,
|
||||
values.color13,
|
||||
values.color14,
|
||||
values.color15,
|
||||
],
|
||||
colors,
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@types/js-yaml": "^3.9.0",
|
||||
"@types/node": "^7.0.37",
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"@types/winston": "^2.3.6",
|
||||
"axios": "0.16.2",
|
||||
"bootstrap": "4.0.0-alpha.6",
|
||||
"core-js": "^2.4.1",
|
||||
@@ -39,7 +40,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^1.5.0",
|
||||
"js-yaml": "^3.9.0"
|
||||
"js-yaml": "^3.9.0",
|
||||
"winston": "^2.4.0"
|
||||
},
|
||||
"false": {}
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ title-bar(
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
|
||||
.drag-space(*ngIf='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
.drag-space([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
.btn-group
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
|
@@ -7,6 +7,7 @@
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
will-change: transform;
|
||||
cursor: default;
|
||||
animation: 0.5s ease-out fadeIn;
|
||||
}
|
||||
@@ -55,15 +56,20 @@ $tab-border-radius: 4px;
|
||||
}
|
||||
|
||||
&>.drag-space {
|
||||
min-width: 100px;
|
||||
flex: 1 0 25%;
|
||||
min-width: 1px;
|
||||
flex: 1 0 1%;
|
||||
-webkit-app-region: drag;
|
||||
|
||||
&.persistent {
|
||||
min-width: 100px;
|
||||
flex: 1 0 25%;
|
||||
}
|
||||
}
|
||||
|
||||
&.inset {
|
||||
padding-left: 85px;
|
||||
}
|
||||
|
||||
|
||||
window-controls {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
@@ -170,7 +170,7 @@ export class AppRootComponent {
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.toolbarButtonProviders.forEach((provider) => {
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
return buttons
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import * as os from 'os'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
@Component({
|
||||
@@ -13,13 +14,14 @@ export class StartPageComponent {
|
||||
|
||||
constructor (
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
) {
|
||||
this.version = electron.app.getVersion()
|
||||
}
|
||||
|
||||
getButtons (): IToolbarButton[] {
|
||||
return this.toolbarButtonProviders
|
||||
return this.config.enabledServices(this.toolbarButtonProviders)
|
||||
.map(provider => provider.provide())
|
||||
.reduce((a, b) => a.concat(b))
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
|
@@ -10,9 +10,11 @@ hotkeys:
|
||||
next-tab:
|
||||
- 'Ctrl-Shift-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
- ['Ctrl-A', '1']
|
||||
@@ -43,3 +45,4 @@ hotkeys:
|
||||
tab-10:
|
||||
- 'Alt-0'
|
||||
- ['Ctrl-A', '0']
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -3,43 +3,29 @@ hotkeys:
|
||||
- 'Ctrl+Space'
|
||||
close-tab:
|
||||
- '⌘-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
toggle-last-tab:
|
||||
- ['Ctrl-A', 'A']
|
||||
- ['Ctrl-A', 'Ctrl-A']
|
||||
toggle-last-tab: []
|
||||
next-tab:
|
||||
- '⌘-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- '⌘-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- '⌘-1'
|
||||
- ['Ctrl-A', '1']
|
||||
tab-2:
|
||||
- '⌘-2'
|
||||
- ['Ctrl-A', '2']
|
||||
tab-3:
|
||||
- '⌘-3'
|
||||
- ['Ctrl-A', '3']
|
||||
tab-4:
|
||||
- '⌘-4'
|
||||
- ['Ctrl-A', '4']
|
||||
tab-5:
|
||||
- '⌘-5'
|
||||
- ['Ctrl-A', '5']
|
||||
tab-6:
|
||||
- '⌘-6'
|
||||
- ['Ctrl-A', '6']
|
||||
tab-7:
|
||||
- '⌘-7'
|
||||
- ['Ctrl-A', '7']
|
||||
tab-8:
|
||||
- '⌘-8'
|
||||
- ['Ctrl-A', '8']
|
||||
tab-9:
|
||||
- '⌘-9'
|
||||
- ['Ctrl-A', '9']
|
||||
tab-10:
|
||||
- '⌘-0'
|
||||
- ['Ctrl-A', '0']
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -10,9 +10,11 @@ hotkeys:
|
||||
next-tab:
|
||||
- 'Ctrl-Shift-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
- ['Ctrl-A', '1']
|
||||
@@ -43,3 +45,4 @@ hotkeys:
|
||||
tab-10:
|
||||
- 'Alt-0'
|
||||
- ['Ctrl-A', '0']
|
||||
pluginBlacklist: []
|
||||
|
@@ -3,6 +3,7 @@ appearance:
|
||||
dockScreen: current
|
||||
dockFill: 50
|
||||
tabsLocation: top
|
||||
cycleTabs: true
|
||||
theme: Standard
|
||||
frame: thin
|
||||
css: '/* * { color: blue !important; } */'
|
||||
|
@@ -3,6 +3,7 @@ import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angul
|
||||
import { DefaultTabProvider } from '../api/defaultTabProvider'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
|
||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
|
||||
@@ -18,6 +19,7 @@ export class AppService {
|
||||
constructor (
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Optional() private defaultTabProvider: DefaultTabProvider,
|
||||
private config: ConfigService,
|
||||
private injector: Injector,
|
||||
log: LogService,
|
||||
) {
|
||||
@@ -70,16 +72,24 @@ export class AppService {
|
||||
}
|
||||
|
||||
nextTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
if (this.tabs.length > 1) {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.selectTab(this.tabs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
if (this.tabs.length > 1) {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
this.selectTab(this.tabs[this.tabs.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import { ConfigProvider } from '../api/configProvider'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { HostAppService } from './hostApp.service'
|
||||
import * as Reflect from 'core-js/es7/reflect'
|
||||
|
||||
const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s })
|
||||
|
||||
@@ -57,6 +58,7 @@ export class ConfigService {
|
||||
private _store: any
|
||||
private path: string
|
||||
private defaults: any
|
||||
private servicesCache: { [id: string]: Function[] } = null
|
||||
|
||||
constructor (
|
||||
electron: ElectronService,
|
||||
@@ -98,4 +100,28 @@ export class ConfigService {
|
||||
requestRestart (): void {
|
||||
this.restartRequested = true
|
||||
}
|
||||
|
||||
enabledServices<T> (services: T[]): T[] {
|
||||
if (!this.servicesCache) {
|
||||
this.servicesCache = {}
|
||||
let ngModule = Reflect.getMetadata('annotations', window['rootModule'])[0]
|
||||
for (let imp of ngModule.imports) {
|
||||
let module = imp['module'] || imp
|
||||
let annotations = Reflect.getMetadata('annotations', module)
|
||||
if (annotations) {
|
||||
this.servicesCache[module['pluginName']] = annotations[0].providers.map(provider => {
|
||||
return provider['useClass'] || provider
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return services.filter(service => {
|
||||
for (let pluginName in this.servicesCache) {
|
||||
if (this.servicesCache[pluginName].includes(service.constructor)) {
|
||||
return !this.store.pluginBlacklist.includes(pluginName)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -74,7 +74,7 @@ export class HostAppService {
|
||||
}
|
||||
|
||||
openDevTools () {
|
||||
this.getWindow().webContents.openDevTools()
|
||||
this.getWindow().webContents.openDevTools({ mode: 'undocked' })
|
||||
}
|
||||
|
||||
focusWindow () {
|
||||
|
@@ -42,7 +42,7 @@ export class HotkeysService {
|
||||
}
|
||||
})
|
||||
})
|
||||
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.hotkeyDescriptions = this.config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.config.changed$.subscribe(() => {
|
||||
this.registerGlobalHotkey()
|
||||
})
|
||||
|
@@ -1,12 +1,47 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ElectronService } from './electron.service'
|
||||
import * as winston from 'winston'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const initializeWinston = (electron: ElectronService) => {
|
||||
const logDirectory = electron.app.getPath('userData')
|
||||
|
||||
if (!fs.existsSync(logDirectory)) {
|
||||
fs.mkdirSync(logDirectory)
|
||||
}
|
||||
|
||||
return new winston.Logger({
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
level: 'debug',
|
||||
filename: path.join(logDirectory, 'log.txt'),
|
||||
handleExceptions: false,
|
||||
json: false,
|
||||
maxsize: 5242880,
|
||||
maxFiles: 5,
|
||||
colorize: false
|
||||
}),
|
||||
new winston.transports.Console({
|
||||
level: 'info',
|
||||
handleExceptions: false,
|
||||
json: false,
|
||||
colorize: true
|
||||
})
|
||||
],
|
||||
exitOnError: false
|
||||
})
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
constructor (
|
||||
private winstonLogger: any,
|
||||
private name: string,
|
||||
) {}
|
||||
|
||||
doLog (level: string, ...args: any[]) {
|
||||
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
|
||||
this.winstonLogger[level](...args)
|
||||
}
|
||||
|
||||
debug (...args: any[]) { this.doLog('debug', ...args) }
|
||||
@@ -18,7 +53,13 @@ export class Logger {
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
private log: any
|
||||
|
||||
constructor (electron: ElectronService) {
|
||||
this.log = initializeWinston(electron)
|
||||
}
|
||||
|
||||
create (name: string): Logger {
|
||||
return new Logger(name)
|
||||
return new Logger(this.log, name)
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
|
||||
@Injectable()
|
||||
export class TabRecoveryService {
|
||||
@@ -11,6 +12,7 @@ export class TabRecoveryService {
|
||||
constructor (
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
log: LogService
|
||||
) {
|
||||
this.logger = log.create('tabRecovery')
|
||||
@@ -31,7 +33,7 @@ export class TabRecoveryService {
|
||||
if (window.localStorage.tabsRecovery) {
|
||||
let tabs: RecoveredTab[] = []
|
||||
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
|
||||
for (let provider of this.tabRecoveryProviders) {
|
||||
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
||||
try {
|
||||
let tab = await provider.recover(token)
|
||||
if (tab) {
|
||||
|
@@ -17,7 +17,7 @@ export class ThemesService {
|
||||
}
|
||||
|
||||
findTheme (name: string): Theme {
|
||||
return this.themes.find(x => x.name === name)
|
||||
return this.config.enabledServices(this.themes).find(x => x.name === name)
|
||||
}
|
||||
|
||||
findCurrentTheme (): Theme {
|
||||
|
@@ -45,6 +45,7 @@ module.exports = {
|
||||
'path',
|
||||
'deepmerge',
|
||||
'untildify',
|
||||
'winston',
|
||||
'js-yaml',
|
||||
/^rxjs/,
|
||||
/^@angular/,
|
||||
|
@@ -6,7 +6,7 @@
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.9.1.tgz#2f3c142771bb345829ce690c5838760b6b9ba553"
|
||||
|
||||
"@types/node@^7.0.37":
|
||||
"@types/node@*", "@types/node@^7.0.37":
|
||||
version "7.0.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c"
|
||||
|
||||
@@ -14,12 +14,22 @@
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.1.tgz#b45c222e24301bd006e3edfc762cc6b51bda236a"
|
||||
|
||||
"@types/winston@^2.3.6":
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/winston/-/winston-2.3.6.tgz#0f0954b9e16abd40598dc6e9cc2ea43044237997"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
async@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
|
||||
|
||||
axios@0.16.2:
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
|
||||
@@ -44,10 +54,18 @@ bootstrap@4.0.0-alpha.6:
|
||||
jquery ">=1.9.1"
|
||||
tether "^1.4.0"
|
||||
|
||||
colors@1.0.x:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||
|
||||
core-js@^2.4.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
|
||||
|
||||
cycle@1.0.x:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
|
||||
|
||||
debug@^2.4.5:
|
||||
version "2.6.8"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
|
||||
@@ -97,6 +115,10 @@ esprima@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
|
||||
|
||||
eyes@0.1.x:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
|
||||
|
||||
follow-redirects@^1.2.3:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea"
|
||||
@@ -126,6 +148,10 @@ is-buffer@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
|
||||
|
||||
isstream@0.1.x:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
|
||||
jquery@>=1.9.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
|
||||
@@ -191,6 +217,10 @@ sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
||||
stack-trace@0.0.x:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
|
||||
tether@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.0.tgz#0f9fa171f75bf58485d8149e94799d7ae74d1c1a"
|
||||
@@ -209,6 +239,17 @@ uuid-1345@^0.99.6:
|
||||
dependencies:
|
||||
macaddress "^0.2.7"
|
||||
|
||||
winston@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.0.tgz#808050b93d52661ed9fb6c26b3f0c826708b0aee"
|
||||
dependencies:
|
||||
async "~1.0.0"
|
||||
colors "1.0.x"
|
||||
cycle "1.0.x"
|
||||
eyes "0.1.x"
|
||||
isstream "0.1.x"
|
||||
stack-trace "0.0.x"
|
||||
|
||||
xelement@^1.0.16:
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/xelement/-/xelement-1.0.16.tgz#900bb46c20fc2dffadff778a9d2dc36699d0ff7e"
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "terminus-plugin-manager",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"description": "Terminus' plugin manager",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
|
@@ -10,34 +10,23 @@ h3 Installed
|
||||
|
||||
.list-group
|
||||
ng-container(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
|
||||
.list-group-item.flex-column.align-items-start(*ngIf='knownUpgrades[plugin.name]')
|
||||
.list-group-item.flex-column.align-items-start
|
||||
.d-flex.w-100
|
||||
.mr-auto.d-flex.flex-column
|
||||
strong {{plugin.name}}
|
||||
small.text-muted.mb-0((click)='showPluginInfo(plugin)') {{plugin.description}}
|
||||
a.text-muted.mb-0((click)='showPluginInfo(plugin)')
|
||||
small {{plugin.description}}
|
||||
.d-flex.flex-column.align-items-end.mr-3
|
||||
div {{plugin.version}}
|
||||
small.text-muted {{plugin.author}}
|
||||
button.btn.btn-outline-primary(
|
||||
*ngIf='npmInstalled',
|
||||
*ngIf='npmInstalled && knownUpgrades[plugin.name]',
|
||||
(click)='upgradePlugin(plugin)',
|
||||
[disabled]='busy[plugin.name] != undefined'
|
||||
)
|
||||
i.fa.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing')
|
||||
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
|
||||
span Upgrade ({{knownUpgrades[plugin.name].version}})
|
||||
|
||||
ng-container(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
|
||||
.list-group-item.flex-column.align-items-start(*ngIf='!knownUpgrades[plugin.name]')
|
||||
.d-flex.w-100
|
||||
.mr-auto.d-flex.flex-column
|
||||
strong {{plugin.name}}
|
||||
a.text-muted.mb-0((click)='showPluginInfo(plugin)')
|
||||
small {{plugin.description}}
|
||||
.d-flex.flex-column.align-items-end.mr-3
|
||||
div {{plugin.version}}
|
||||
small.text-muted {{plugin.author}}
|
||||
i.fa.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official')
|
||||
button.btn.btn-outline-danger(
|
||||
(click)='uninstallPlugin(plugin)',
|
||||
*ngIf='!plugin.isBuiltin && npmInstalled',
|
||||
@@ -45,6 +34,16 @@ h3 Installed
|
||||
)
|
||||
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')
|
||||
button.btn.btn-outline-danger(
|
||||
*ngIf='config.store.pluginBlacklist.includes(plugin.name)',
|
||||
(click)='enablePlugin(plugin)'
|
||||
)
|
||||
i.fa.fa-fw.fa-play
|
||||
button.btn.btn-outline-primary(
|
||||
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
|
||||
(click)='disablePlugin(plugin)'
|
||||
)
|
||||
i.fa.fa-fw.fa-pause
|
||||
|
||||
.text-center.mt-5(*ngIf='npmMissing')
|
||||
h4 npm not installed
|
||||
|
@@ -105,4 +105,16 @@ export class PluginsSettingsTabComponent {
|
||||
showPluginInfo (plugin: IPluginInfo) {
|
||||
this.electron.shell.openExternal('https://www.npmjs.com/package/' + plugin.packageName)
|
||||
}
|
||||
|
||||
enablePlugin (plugin: IPluginInfo) {
|
||||
this.config.store.pluginBlacklist = this.config.store.pluginBlacklist.filter(x => x !== plugin.name)
|
||||
this.config.save()
|
||||
this.config.requestRestart()
|
||||
}
|
||||
|
||||
disablePlugin (plugin: IPluginInfo) {
|
||||
this.config.store.pluginBlacklist.push(plugin.name)
|
||||
this.config.save()
|
||||
this.config.requestRestart()
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider, ComponentType } from 'terminus-settings'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class PluginsSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'plugins'
|
||||
title = 'Plugins'
|
||||
|
||||
getComponentType (): ComponentType {
|
||||
getComponentType (): any {
|
||||
return PluginsSettingsTabComponent
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "terminus-settings",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"description": "Terminus terminal settings page",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
|
@@ -1,11 +1,8 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
export declare type ComponentType = new (...args: any[]) => Component
|
||||
|
||||
export abstract class SettingsTabProvider {
|
||||
id: string
|
||||
title: string
|
||||
|
||||
getComponentType (): ComponentType {
|
||||
getComponentType (): any {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
|
||||
|
||||
ngb-tabset.vertical(type='tabs')
|
||||
ngb-tab
|
||||
ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
|
||||
ngb-tab(id='application')
|
||||
ng-template(ngbTabTitle)
|
||||
| Application
|
||||
ng-template(ngbTabContent)
|
||||
@@ -164,7 +164,7 @@ ngb-tabset.vertical(type='tabs')
|
||||
'(ngModelChange)'='config.save()',
|
||||
)
|
||||
|
||||
ngb-tab
|
||||
ngb-tab(id='hotkeys')
|
||||
ng-template(ngbTabTitle)
|
||||
| Hotkeys
|
||||
ng-template(ngbTabContent)
|
||||
@@ -184,7 +184,7 @@ ngb-tabset.vertical(type='tabs')
|
||||
'(modelChange)'='config.save(); docking.dock()'
|
||||
)
|
||||
|
||||
ngb-tab(*ngFor='let provider of settingsProviders')
|
||||
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
|
||||
ng-template(ngbTabTitle)
|
||||
| {{provider.title}}
|
||||
ng-template(ngbTabContent)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { Component, Inject, Input } from '@angular/core'
|
||||
import { ElectronService, DockingService, ConfigService, IHotkeyDescription, HotkeyProvider, BaseTabComponent, Theme, HostAppService } from 'terminus-core'
|
||||
|
||||
import { SettingsTabProvider } from '../api'
|
||||
@@ -12,6 +12,7 @@ import { SettingsTabProvider } from '../api'
|
||||
],
|
||||
})
|
||||
export class SettingsTabComponent extends BaseTabComponent {
|
||||
@Input() activeTab: string
|
||||
hotkeyFilter = ''
|
||||
private hotkeyDescriptions: IHotkeyDescription[]
|
||||
private screens
|
||||
@@ -26,10 +27,12 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
@Inject(Theme) public themes: Theme[],
|
||||
) {
|
||||
super()
|
||||
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.title = 'Settings'
|
||||
this.scrollable = true
|
||||
this.screens = this.docking.getScreens()
|
||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||
this.themes = config.enabledServices(this.themes)
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
|
@@ -40,3 +40,4 @@ export default class SettingsModule {
|
||||
}
|
||||
|
||||
export * from './api'
|
||||
export { SettingsTabComponent }
|
||||
|
48
terminus-ssh/package.json
Normal file
48
terminus-ssh/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "terminus-ssh",
|
||||
"version": "0.0.1",
|
||||
"description": "SSH connection manager for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --color",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/ssh2": "^0.5.35",
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"apply-loader": "^2.0.0",
|
||||
"awesome-typescript-loader": "^3.1.2",
|
||||
"electron": "^1.6.11",
|
||||
"ngx-toastr": "^8.0.0",
|
||||
"pug": "^2.0.0-rc.3",
|
||||
"pug-loader": "^2.3.0",
|
||||
"rxjs": "^5.4.0",
|
||||
"typescript": "^2.2.2",
|
||||
"webpack": "^2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^4.1.3",
|
||||
"@angular/core": "^4.1.3",
|
||||
"@angular/forms": "^4.1.3",
|
||||
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.29",
|
||||
"terminus-core": "*",
|
||||
"terminus-settings": "*",
|
||||
"terminus-terminal": "*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wincredmgr": "^2.0.0",
|
||||
"xkeychain": "^0.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"ssh2": "^0.5.5"
|
||||
}
|
||||
}
|
52
terminus-ssh/src/api.ts
Normal file
52
terminus-ssh/src/api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BaseSession } from 'terminus-terminal'
|
||||
|
||||
export interface SSHConnection {
|
||||
name?: string
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
password?: string
|
||||
privateKey?: string
|
||||
}
|
||||
|
||||
export class SSHSession extends BaseSession {
|
||||
constructor (private shell: any) {
|
||||
super()
|
||||
|
||||
this.open = true
|
||||
|
||||
this.shell.on('data', data => {
|
||||
this.emitOutput(data.toString())
|
||||
})
|
||||
|
||||
this.shell.on('end', () => {
|
||||
if (this.open) {
|
||||
this.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resize (columns, rows) {
|
||||
this.shell.setWindow(rows, columns)
|
||||
}
|
||||
|
||||
write (data) {
|
||||
this.shell.write(data)
|
||||
}
|
||||
|
||||
kill (signal?: string) {
|
||||
this.shell.signal(signal || 'TERM')
|
||||
}
|
||||
|
||||
async getChildProcesses (): Promise<any[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
async gracefullyKillProcess (): Promise<void> {
|
||||
this.kill('TERM')
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string> {
|
||||
return null
|
||||
}
|
||||
}
|
37
terminus-ssh/src/buttonProvider.ts
Normal file
37
terminus-ssh/src/buttonProvider.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
||||
import { SSHModalComponent } from './components/sshModal.component'
|
||||
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private ngbModal: NgbModal,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||
if (hotkey === 'ssh') {
|
||||
this.activate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
activate () {
|
||||
let modal = this.ngbModal.open(SSHModalComponent)
|
||||
modal.result.then(() => {
|
||||
//this.terminal.openTab(shell)
|
||||
})
|
||||
}
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'globe',
|
||||
weight: 5,
|
||||
title: 'SSH connections',
|
||||
click: async () => {
|
||||
this.activate()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
.modal-body
|
||||
.form-group
|
||||
label Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.name',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Host
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.host',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Port
|
||||
input.form-control(
|
||||
type='number',
|
||||
placeholder='22',
|
||||
[(ngModel)]='connection.post',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Username
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.user',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Private key
|
||||
.input-group
|
||||
input.form-control(
|
||||
type='text',
|
||||
placeholder='Key file path',
|
||||
[(ngModel)]='connection.privateKey'
|
||||
)
|
||||
.input-group-btn
|
||||
button.btn.btn-secondary((click)='selectPrivateKey()')
|
||||
i.fa.fa-folder-open
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
37
terminus-ssh/src/components/editConnectionModal.component.ts
Normal file
37
terminus-ssh/src/components/editConnectionModal.component.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ElectronService, HostAppService } from 'terminus-core'
|
||||
import { SSHConnection } from '../api'
|
||||
|
||||
@Component({
|
||||
template: require('./editConnectionModal.component.pug'),
|
||||
})
|
||||
export class EditConnectionModalComponent {
|
||||
connection: SSHConnection
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal,
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
) { }
|
||||
|
||||
selectPrivateKey () {
|
||||
let path = this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
title: 'Select private key',
|
||||
}
|
||||
)
|
||||
if (path) {
|
||||
this.connection.privateKey = path[0]
|
||||
}
|
||||
}
|
||||
|
||||
save () {
|
||||
this.modalInstance.close(this.connection)
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
}
|
9
terminus-ssh/src/components/promptModal.component.pug
Normal file
9
terminus-ssh/src/components/promptModal.component.pug
Normal file
@@ -0,0 +1,9 @@
|
||||
.modal-body
|
||||
input.form-control(
|
||||
[type]='password ? "password" : "text"',
|
||||
[(ngModel)]='value',
|
||||
#input,
|
||||
[placeholder]='prompt',
|
||||
(keyup.enter)='ok()',
|
||||
(keyup.esc)='cancel()',
|
||||
)
|
27
terminus-ssh/src/components/promptModal.component.ts
Normal file
27
terminus-ssh/src/components/promptModal.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component, Input, ViewChild, ElementRef } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
template: require('./promptModal.component.pug'),
|
||||
})
|
||||
export class PromptModalComponent {
|
||||
@Input() value: string
|
||||
@Input() password: boolean
|
||||
@ViewChild('input') input: ElementRef
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.input.nativeElement.focus()
|
||||
}
|
||||
|
||||
ok () {
|
||||
this.modalInstance.close(this.value)
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalInstance.close('')
|
||||
}
|
||||
}
|
24
terminus-ssh/src/components/sshModal.component.pug
Normal file
24
terminus-ssh/src/components/sshModal.component.pug
Normal file
@@ -0,0 +1,24 @@
|
||||
.modal-body
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='quickTarget',
|
||||
autofocus,
|
||||
placeholder='Quick connect: [user@]host[:port]',
|
||||
(keyup.enter)='quickConnect()'
|
||||
)
|
||||
|
||||
.list-group.mt-3(*ngIf='lastConnection')
|
||||
a.list-group-item.list-group-item-action((click)='connect(lastConnection)')
|
||||
i.fa.fa-fw.fa-history
|
||||
span {{lastConnection.name}}
|
||||
|
||||
.list-group.mt-3
|
||||
a.list-group-item.list-group-item-action(*ngFor='let connection of connections', (click)='connect(connection)')
|
||||
i.fa.fa-fw.fa-globe
|
||||
span {{connection.name}}
|
||||
a.list-group-item.list-group-item-action((click)='manageConnections()')
|
||||
i.fa.fa-fw.fa-wrench
|
||||
span Manage connections
|
||||
|
||||
//.modal-footer
|
||||
button.btn.btn-outline-primary((click)='close()') Cancel
|
68
terminus-ssh/src/components/sshModal.component.ts
Normal file
68
terminus-ssh/src/components/sshModal.component.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { ConfigService, AppService } from 'terminus-core'
|
||||
import { SettingsTabComponent } from 'terminus-settings'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHConnection } from '../api'
|
||||
|
||||
@Component({
|
||||
template: require('./sshModal.component.pug'),
|
||||
//styles: [require('./sshModal.component.scss')],
|
||||
})
|
||||
export class SSHModalComponent {
|
||||
connections: SSHConnection[]
|
||||
quickTarget: string
|
||||
lastConnection: SSHConnection
|
||||
|
||||
constructor (
|
||||
public modalInstance: NgbActiveModal,
|
||||
private config: ConfigService,
|
||||
private ssh: SSHService,
|
||||
private app: AppService,
|
||||
private toastr: ToastrService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.connections = this.config.store.ssh.connections
|
||||
if (window.localStorage.lastConnection) {
|
||||
this.lastConnection = JSON.parse(window.localStorage.lastConnection)
|
||||
}
|
||||
}
|
||||
|
||||
quickConnect () {
|
||||
let user = 'root'
|
||||
let host = this.quickTarget
|
||||
let port = 22
|
||||
if (host.includes('@')) {
|
||||
[user, host] = host.split('@')
|
||||
}
|
||||
if (host.includes(':')) {
|
||||
port = parseInt(host.split(':')[1])
|
||||
host = host.split(':')[0]
|
||||
}
|
||||
|
||||
let connection: SSHConnection = {
|
||||
name: this.quickTarget,
|
||||
host, user, port
|
||||
}
|
||||
window.localStorage.lastConnection = JSON.stringify(connection)
|
||||
this.connect(connection)
|
||||
}
|
||||
|
||||
connect (connection: SSHConnection) {
|
||||
this.close()
|
||||
this.ssh.connect(connection).catch(error => {
|
||||
this.toastr.error(`Could not connect: ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
manageConnections () {
|
||||
this.close()
|
||||
this.app.openNewTab(SettingsTabComponent, { activeTab: 'ssh' })
|
||||
}
|
||||
|
||||
close () {
|
||||
this.modalInstance.close()
|
||||
}
|
||||
}
|
15
terminus-ssh/src/components/sshSettingsTab.component.pug
Normal file
15
terminus-ssh/src/components/sshSettingsTab.component.pug
Normal file
@@ -0,0 +1,15 @@
|
||||
h3 Connections
|
||||
|
||||
.list-group.mt-3.mb-3
|
||||
.list-group-item(*ngFor='let connection of connections')
|
||||
.d-flex.w-100
|
||||
.mr-auto
|
||||
div
|
||||
span {{connection.name}}
|
||||
.text-muted {{connection.host}}
|
||||
button.btn.btn-outline-info.ml-2((click)='editConnection(connection)')
|
||||
i.fa.fa-pencil
|
||||
button.btn.btn-outline-danger.ml-1((click)='deleteConnection(connection)')
|
||||
i.fa.fa-trash-o
|
||||
|
||||
button.btn.btn-outline-primary((click)='createConnection()') Add connection
|
52
terminus-ssh/src/components/sshSettingsTab.component.ts
Normal file
52
terminus-ssh/src/components/sshSettingsTab.component.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService } from 'terminus-core'
|
||||
import { SSHConnection } from '../api'
|
||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||
|
||||
@Component({
|
||||
template: require('./sshSettingsTab.component.pug'),
|
||||
})
|
||||
export class SSHSettingsTabComponent {
|
||||
connections: SSHConnection[]
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private ngbModal: NgbModal,
|
||||
) {
|
||||
this.connections = this.config.store.ssh.connections
|
||||
}
|
||||
|
||||
createConnection () {
|
||||
let connection: SSHConnection = {
|
||||
name: '',
|
||||
host: '',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
}
|
||||
let modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||
modal.componentInstance.connection = connection
|
||||
modal.result.then(result => {
|
||||
this.connections.push(result)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
})
|
||||
}
|
||||
|
||||
editConnection (connection: SSHConnection) {
|
||||
let modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||
modal.componentInstance.connection = Object.assign({}, connection)
|
||||
modal.result.then(result => {
|
||||
Object.assign(connection, result)
|
||||
this.config.save()
|
||||
})
|
||||
}
|
||||
|
||||
deleteConnection (connection: SSHConnection) {
|
||||
if (confirm(`Delete "${connection.name}"?`)) {
|
||||
this.connections = this.connections.filter(x => x !== connection)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
}
|
||||
}
|
||||
}
|
18
terminus-ssh/src/config.ts
Normal file
18
terminus-ssh/src/config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ConfigProvider } from 'terminus-core'
|
||||
|
||||
export class SSHConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
ssh: {
|
||||
connections: [],
|
||||
options: {
|
||||
}
|
||||
},
|
||||
hotkeys: {
|
||||
'ssh': [
|
||||
'Alt-S',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
platformDefaults = { }
|
||||
}
|
47
terminus-ssh/src/index.ts
Normal file
47
terminus-ssh/src/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
import { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
||||
import { SSHModalComponent } from './components/sshModal.component'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
import { SSHService } from './services/ssh.service'
|
||||
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SSHConfigProvider } from './config'
|
||||
import { SSHSettingsTabProvider } from './settings'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
NgbModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ToastrModule,
|
||||
],
|
||||
providers: [
|
||||
PasswordStorageService,
|
||||
SSHService,
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
EditConnectionModalComponent,
|
||||
PromptModalComponent,
|
||||
SSHModalComponent,
|
||||
SSHSettingsTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
EditConnectionModalComponent,
|
||||
PromptModalComponent,
|
||||
SSHModalComponent,
|
||||
SSHSettingsTabComponent,
|
||||
],
|
||||
})
|
||||
export default class SSHModule { }
|
73
terminus-ssh/src/services/passwordStorage.service.ts
Normal file
73
terminus-ssh/src/services/passwordStorage.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { SSHConnection } from '../api'
|
||||
|
||||
let xkeychain
|
||||
let wincredmgr
|
||||
try {
|
||||
xkeychain = require('xkeychain')
|
||||
} catch (error) {
|
||||
try {
|
||||
wincredmgr = require('wincredmgr')
|
||||
} catch (error2) {
|
||||
console.warn('No keychain manager available')
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PasswordStorageService {
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
) { }
|
||||
|
||||
savePassword (connection: SSHConnection, password: string) {
|
||||
if (xkeychain) {
|
||||
xkeychain.setPassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
password
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.WriteCredentials(
|
||||
'user',
|
||||
password,
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deletePassword (connection: SSHConnection) {
|
||||
if (xkeychain) {
|
||||
xkeychain.deletePassword({
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
}, () => null)
|
||||
} else {
|
||||
wincredmgr.DeleteCredentials(
|
||||
`ssh:${connection.user}@${connection.host}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadPassword (connection: SSHConnection): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
if (!wincredmgr && !xkeychain.isSupported()) {
|
||||
return resolve(null)
|
||||
}
|
||||
if (xkeychain) {
|
||||
xkeychain.getPassword(
|
||||
{
|
||||
account: connection.user,
|
||||
service: `ssh@${connection.host}`,
|
||||
},
|
||||
(_, result) => this.zone.run(() => resolve(result))
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
|
||||
} catch (error) {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
167
terminus-ssh/src/services/ssh.service.ts
Normal file
167
terminus-ssh/src/services/ssh.service.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Client } from 'ssh2'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
||||
import { TerminalTabComponent } from 'terminus-terminal'
|
||||
import { SSHConnection, SSHSession } from '../api'
|
||||
import { PromptModalComponent } from '../components/promptModal.component'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
const { SSH2Stream } = require('ssh2-streams')
|
||||
|
||||
@Injectable()
|
||||
export class SSHService {
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
private app: AppService,
|
||||
private zone: NgZone,
|
||||
private ngbModal: NgbModal,
|
||||
private hostApp: HostAppService,
|
||||
private passwordStorage: PasswordStorageService,
|
||||
private toastr: ToastrService,
|
||||
) {
|
||||
this.logger = log.create('ssh')
|
||||
}
|
||||
|
||||
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
||||
let privateKey: string = null
|
||||
let privateKeyPassphrase: string = null
|
||||
let privateKeyPath = connection.privateKey
|
||||
if (!privateKeyPath) {
|
||||
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
this.logger.info('Using user\'s default private key:', userKeyPath)
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyPath) {
|
||||
try {
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
this.toastr.warning('Could not read the private key file')
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
this.logger.info('Loaded private key from', privateKeyPath)
|
||||
|
||||
if (privateKey.includes('ENCRYPTED')) {
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
try {
|
||||
privateKeyPassphrase = await modal.result
|
||||
} catch (_err) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssh = new Client()
|
||||
let connected = false
|
||||
let savedPassword: string = null
|
||||
await new Promise((resolve, reject) => {
|
||||
ssh.on('ready', () => {
|
||||
connected = true
|
||||
if (savedPassword) {
|
||||
this.passwordStorage.savePassword(connection, savedPassword)
|
||||
}
|
||||
this.zone.run(resolve)
|
||||
})
|
||||
ssh.on('error', error => {
|
||||
this.passwordStorage.deletePassword(connection)
|
||||
this.zone.run(() => {
|
||||
if (connected) {
|
||||
this.toastr.error(error.toString())
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||
console.log(name, instructions, instructionsLang)
|
||||
let results = []
|
||||
for (let prompt of prompts) {
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = prompt.prompt
|
||||
modal.componentInstance.password = !prompt.echo
|
||||
results.push(await modal.result)
|
||||
}
|
||||
finish(results)
|
||||
}))
|
||||
|
||||
let agent: string = null
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
agent = 'pageant'
|
||||
} else {
|
||||
agent = process.env.SSH_AUTH_SOCK
|
||||
}
|
||||
|
||||
ssh.connect({
|
||||
host: connection.host,
|
||||
port: connection.port || 22,
|
||||
username: connection.user,
|
||||
password: connection.privateKey ? undefined : '',
|
||||
privateKey,
|
||||
passphrase: privateKeyPassphrase,
|
||||
tryKeyboard: true,
|
||||
agent,
|
||||
agentForward: !!agent,
|
||||
})
|
||||
|
||||
let keychainPasswordUsed = false
|
||||
|
||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||
if (connection.password) {
|
||||
this.logger.info('Using preset password')
|
||||
return connection.password
|
||||
}
|
||||
|
||||
if (!keychainPasswordUsed) {
|
||||
let password = await this.passwordStorage.loadPassword(connection)
|
||||
if (password) {
|
||||
this.logger.info('Using saved password')
|
||||
keychainPasswordUsed = true
|
||||
return password
|
||||
}
|
||||
}
|
||||
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
|
||||
modal.componentInstance.password = true
|
||||
savedPassword = await modal.result
|
||||
return savedPassword
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
let shell = await new Promise((resolve, reject) => {
|
||||
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(shell)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let session = new SSHSession(shell)
|
||||
|
||||
return this.zone.run(() => this.app.openNewTab(
|
||||
TerminalTabComponent,
|
||||
{ session, sessionOptions: {} }
|
||||
) as TerminalTabComponent)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _authPassword = SSH2Stream.prototype.authPassword
|
||||
SSH2Stream.prototype.authPassword = async function (username, passwordFn) {
|
||||
_authPassword.bind(this)(username, await passwordFn())
|
||||
}
|
14
terminus-ssh/src/settings.ts
Normal file
14
terminus-ssh/src/settings.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class SSHSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'ssh'
|
||||
title = 'SSH'
|
||||
|
||||
getComponentType (): any {
|
||||
return SSHSettingsTabComponent
|
||||
}
|
||||
}
|
12
terminus-ssh/tsconfig.json
Normal file
12
terminus-ssh/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"declarationDir": "dist",
|
||||
"paths": {
|
||||
"terminus-*": ["terminus-*"],
|
||||
"*": ["app/node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
49
terminus-ssh/webpack.config.js
Normal file
49
terminus-ssh/webpack.config.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
entry: 'src/index.ts',
|
||||
devtool: 'source-map',
|
||||
context: __dirname,
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'index.js',
|
||||
pathinfo: true,
|
||||
libraryTarget: 'umd',
|
||||
devtoolModuleFilenameTemplate: 'webpack-terminus-ssh:///[resource-path]',
|
||||
},
|
||||
resolve: {
|
||||
modules: ['.', 'src', 'node_modules', '../app/node_modules'].map(x => path.join(__dirname, x)),
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
query: {
|
||||
configFileName: path.resolve(__dirname, 'tsconfig.json'),
|
||||
typeRoots: [path.resolve(__dirname, 'node_modules/@types')],
|
||||
paths: {
|
||||
"terminus-*": [path.resolve(__dirname, '../terminus-*')],
|
||||
"*": [path.resolve(__dirname, '../app/node_modules/*')],
|
||||
}
|
||||
}
|
||||
},
|
||||
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
]
|
||||
},
|
||||
externals: [
|
||||
'fs',
|
||||
'node-ssh',
|
||||
'xkeychain',
|
||||
'wincredmgr',
|
||||
'path',
|
||||
'ngx-toastr',
|
||||
/^rxjs/,
|
||||
/^@angular/,
|
||||
/^@ng-bootstrap/,
|
||||
/^terminus-/,
|
||||
]
|
||||
}
|
2996
terminus-ssh/yarn.lock
Normal file
2996
terminus-ssh/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "terminus-terminal",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"description": "Terminus' terminal emulation core",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
|
@@ -16,7 +16,13 @@ export class HyperColorSchemes extends TerminalColorSchemeProvider {
|
||||
try {
|
||||
let module = (global as any).require(path.join(pluginsPath, plugin))
|
||||
if (module.decorateConfig) {
|
||||
let config = module.decorateConfig({})
|
||||
let config: any
|
||||
try {
|
||||
config = module.decorateConfig({})
|
||||
} catch (error) {
|
||||
console.warn('Could not load Hyper theme:', plugin)
|
||||
return
|
||||
}
|
||||
if (config.colors) {
|
||||
themes.push({
|
||||
name: plugin,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
h3.mb-2 Appearance
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
label Preview
|
||||
.appearance-preview(
|
||||
[style.font-family]='config.store.terminal.font',
|
||||
[style.font-size]='config.store.terminal.fontSize + "px"',
|
||||
@@ -60,23 +60,6 @@
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[15]') W
|
||||
div
|
||||
span
|
||||
div
|
||||
span john@doe-pc
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[1]') $
|
||||
span webpack
|
||||
div
|
||||
span Asset Size
|
||||
div
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[2]') main.js
|
||||
span 234 kB
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[2]') [emitted]
|
||||
div
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[3]') big.js
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[3]') 1.2 MB
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[2]') [emitted]
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[3]') [big]
|
||||
div
|
||||
span
|
||||
div
|
||||
span john@doe-pc
|
||||
span([style.color]='config.store.terminal.colorScheme.colors[1]') $
|
||||
@@ -228,16 +211,33 @@
|
||||
)
|
||||
| ▁
|
||||
|
||||
.form-group
|
||||
label Shell
|
||||
select.form-control(
|
||||
'[(ngModel)]'='config.store.terminal.shell',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(
|
||||
*ngFor='let shell of shells',
|
||||
[ngValue]='shell.id'
|
||||
) {{shell.name}}
|
||||
h3.mt-2.mb-2 Behaviour
|
||||
|
||||
.row
|
||||
.col-md-6
|
||||
.d-flex
|
||||
.form-group.mr-3
|
||||
label Shell
|
||||
select.form-control(
|
||||
'[(ngModel)]'='config.store.terminal.shell',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(
|
||||
*ngFor='let shell of shells',
|
||||
[ngValue]='shell.id'
|
||||
) {{shell.name}}
|
||||
|
||||
.form-group
|
||||
label Session persistence
|
||||
select.form-control(
|
||||
'[(ngModel)]'='config.store.terminal.persistence',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option([ngValue]='null') Off
|
||||
option(
|
||||
*ngFor='let provider of persistenceProviders',
|
||||
[ngValue]='provider.id'
|
||||
) {{provider.displayName}}
|
||||
|
||||
.form-group(*ngIf='config.store.terminal.shell == "custom"')
|
||||
label Custom shell
|
||||
@@ -247,6 +247,39 @@
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Working directory
|
||||
input.form-control(
|
||||
type='text',
|
||||
placeholder='Home directory',
|
||||
'[(ngModel)]'='config.store.terminal.workingDirectory',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Auto-open a terminal on app start
|
||||
br
|
||||
.btn-group(
|
||||
'[(ngModel)]'='config.store.terminal.autoOpen',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| On
|
||||
|
||||
.col-md-6
|
||||
.d-flex
|
||||
.form-group.mr-3
|
||||
label Terminal bell
|
||||
@@ -301,37 +334,49 @@
|
||||
)
|
||||
| On
|
||||
|
||||
.form-group
|
||||
label Session persistence
|
||||
select.form-control(
|
||||
'[(ngModel)]'='config.store.terminal.persistence',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option([ngValue]='null') Off
|
||||
option(
|
||||
*ngFor='let provider of persistenceProviders',
|
||||
[ngValue]='provider.id'
|
||||
) {{provider.displayName}}
|
||||
.d-flex
|
||||
.form-group.mr-3
|
||||
label Copy on select
|
||||
br
|
||||
.btn-group(
|
||||
'[(ngModel)]'='config.store.terminal.copyOnSelect',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| On
|
||||
|
||||
.form-group
|
||||
label Auto-open a terminal on app start
|
||||
br
|
||||
.btn-group(
|
||||
'[(ngModel)]'='config.store.terminal.autoOpen',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| On
|
||||
.form-group
|
||||
label Right click behaviour
|
||||
br
|
||||
.btn-group(
|
||||
'[(ngModel)]'='config.store.terminal.rightClick',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
value='menu'
|
||||
)
|
||||
| Menu
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
value='paste'
|
||||
)
|
||||
| Paste
|
||||
|
@@ -27,7 +27,7 @@ export class TerminalSettingsTabComponent {
|
||||
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
||||
@Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[],
|
||||
) {
|
||||
this.persistenceProviders = persistenceProviders.filter(x => x.isAvailable())
|
||||
this.persistenceProviders = this.config.enabledServices(persistenceProviders).filter(x => x.isAvailable())
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -46,8 +46,8 @@ export class TerminalSettingsTabComponent {
|
||||
this.fonts.sort()
|
||||
})
|
||||
}
|
||||
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
|
||||
this.shells = (await Promise.all(this.shellProviders.map(x => x.provide()))).reduce((a, b) => a.concat(b))
|
||||
this.colorSchemes = (await Promise.all(this.config.enabledServices(this.colorSchemeProviders).map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
|
||||
this.shells = (await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
fontAutocomplete = (text$: Observable<string>) => {
|
||||
|
@@ -60,23 +60,28 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.decorators = this.decorators || []
|
||||
this.title = 'Terminal'
|
||||
this.resize$.first().subscribe(async (resizeEvent) => {
|
||||
this.session = this.sessions.addSession(
|
||||
Object.assign({}, this.sessionOptions, resizeEvent)
|
||||
)
|
||||
if (!this.session) {
|
||||
this.session = this.sessions.addSession(
|
||||
Object.assign({}, this.sessionOptions, resizeEvent)
|
||||
)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(resizeEvent.width, resizeEvent.height)
|
||||
}, 1000)
|
||||
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
// let data = datas.join('')
|
||||
this.zone.run(() => {
|
||||
this.output$.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
this.write(data)
|
||||
})
|
||||
|
||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
@@ -120,7 +125,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
})
|
||||
|
||||
this.hterm = new hterm.hterm.Terminal()
|
||||
this.decorators.forEach((decorator) => {
|
||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||
decorator.attach(this)
|
||||
})
|
||||
|
||||
@@ -178,7 +183,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
label: 'Paste',
|
||||
click: () => {
|
||||
this.zone.run(() => {
|
||||
this.sendInput(this.electron.clipboard.readText())
|
||||
this.paste()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -223,12 +228,17 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.mouseEvent$.next(event)
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 3) {
|
||||
this.contextMenu.popup({
|
||||
x: event.pageX + this.content.nativeElement.getBoundingClientRect().left,
|
||||
y: event.pageY + this.content.nativeElement.getBoundingClientRect().top,
|
||||
async: true,
|
||||
})
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.contextMenu.popup({
|
||||
x: event.pageX + this.content.nativeElement.getBoundingClientRect().left,
|
||||
y: event.pageY + this.content.nativeElement.getBoundingClientRect().top,
|
||||
async: true,
|
||||
})
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -302,12 +312,16 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.io.writeUTF8(data)
|
||||
}
|
||||
|
||||
paste () {
|
||||
this.sendInput(this.electron.clipboard.readText())
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.hterm.wipeContents()
|
||||
this.hterm.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
async configure (): Promise<void> {
|
||||
configure (): void {
|
||||
let config = this.config.store
|
||||
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
this.setFontSize()
|
||||
@@ -319,7 +333,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
preferenceManager.set('send-encoding', 'raw')
|
||||
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
|
||||
preferenceManager.set('scrollbar-visible', this.hostApp.platform === Platform.macOS)
|
||||
preferenceManager.set('copy-on-select', false)
|
||||
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
|
||||
preferenceManager.set('alt-sends-what', 'browser-key')
|
||||
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
|
||||
preferenceManager.set('pass-alt-number', true)
|
||||
@@ -392,7 +406,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.decorators.forEach(decorator => {
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
|
@@ -13,6 +13,9 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
cursor: 'block',
|
||||
cursorBlink: true,
|
||||
customShell: '',
|
||||
rightClick: 'menu',
|
||||
copyOnSelect: false,
|
||||
workingDirectory: '',
|
||||
colorScheme: {
|
||||
__nonStructural: true,
|
||||
name: 'Material',
|
||||
@@ -80,6 +83,8 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
font: 'Consolas',
|
||||
shell: 'clink',
|
||||
persistence: null,
|
||||
rightClick: 'paste',
|
||||
copyOnSelect: true,
|
||||
},
|
||||
hotkeys: {
|
||||
'copy': [
|
||||
|
@@ -10,7 +10,7 @@ import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component'
|
||||
import { ColorPickerComponent } from './components/colorPicker.component'
|
||||
|
||||
import { SessionsService } from './services/sessions.service'
|
||||
import { SessionsService, BaseSession } from './services/sessions.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
|
||||
import { ScreenPersistenceProvider } from './persistence/screen'
|
||||
@@ -117,4 +117,4 @@ export default class TerminalModule {
|
||||
}
|
||||
|
||||
export * from './api'
|
||||
export { TerminalService }
|
||||
export { TerminalService, BaseSession, TerminalTabComponent }
|
||||
|
@@ -153,8 +153,8 @@ export class TMux {
|
||||
|
||||
async create (id: string, options: SessionOptions): Promise<void> {
|
||||
await this.ready
|
||||
let args = [options.command].concat(options.args)
|
||||
let cmd = args.map(x => `"${x.replace('"', '\\"')}"`)
|
||||
let args = [options.command].concat(options.args.slice(1))
|
||||
let cmd = args.map(x => `"${x.replace('"', '\\"')}"`).join(' ')
|
||||
await this.process.command(
|
||||
`new-session -s "${id}" -d`
|
||||
+ (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '')
|
||||
|
@@ -14,7 +14,7 @@ export interface IChildProcess {
|
||||
command: string
|
||||
}
|
||||
|
||||
export class Session {
|
||||
export abstract class BaseSession {
|
||||
open: boolean
|
||||
name: string
|
||||
output$ = new Subject<string>()
|
||||
@@ -22,11 +22,46 @@ export class Session {
|
||||
destroyed$ = new Subject<void>()
|
||||
recoveryId: string
|
||||
truePID: number
|
||||
private pty: any
|
||||
private initialDataBuffer = ''
|
||||
private initialDataBufferReleased = false
|
||||
|
||||
emitOutput (data: string) {
|
||||
if (!this.initialDataBufferReleased) {
|
||||
this.initialDataBuffer += data
|
||||
} else {
|
||||
this.output$.next(data)
|
||||
}
|
||||
}
|
||||
|
||||
releaseInitialDataBuffer () {
|
||||
this.initialDataBufferReleased = true
|
||||
this.output$.next(this.initialDataBuffer)
|
||||
this.initialDataBuffer = null
|
||||
}
|
||||
|
||||
abstract resize (columns, rows)
|
||||
abstract write (data)
|
||||
abstract kill (signal?: string)
|
||||
abstract async getChildProcesses (): Promise<IChildProcess[]>
|
||||
abstract async gracefullyKillProcess (): Promise<void>
|
||||
abstract async getWorkingDirectory (): Promise<string>
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
if (this.open) {
|
||||
this.open = false
|
||||
this.closed$.next()
|
||||
this.destroyed$.next()
|
||||
this.output$.complete()
|
||||
await this.gracefullyKillProcess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Session extends BaseSession {
|
||||
private pty: any
|
||||
|
||||
constructor (options: SessionOptions) {
|
||||
super()
|
||||
this.name = options.name
|
||||
this.recoveryId = options.recoveryId
|
||||
|
||||
@@ -65,12 +100,8 @@ export class Session {
|
||||
|
||||
this.open = true
|
||||
|
||||
this.pty.on('data', (data) => {
|
||||
if (!this.initialDataBufferReleased) {
|
||||
this.initialDataBuffer += data
|
||||
} else {
|
||||
this.output$.next(data)
|
||||
}
|
||||
this.pty.on('data', data => {
|
||||
this.emitOutput(data)
|
||||
})
|
||||
|
||||
this.pty.on('exit', () => {
|
||||
@@ -86,12 +117,6 @@ export class Session {
|
||||
})
|
||||
}
|
||||
|
||||
releaseInitialDataBuffer () {
|
||||
this.initialDataBufferReleased = true
|
||||
this.output$.next(this.initialDataBuffer)
|
||||
this.initialDataBuffer = null
|
||||
}
|
||||
|
||||
resize (columns, rows) {
|
||||
if (this.pty._writable) {
|
||||
this.pty.resize(columns, rows)
|
||||
@@ -144,16 +169,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
if (this.open) {
|
||||
this.open = false
|
||||
this.closed$.next()
|
||||
this.destroyed$.next()
|
||||
this.output$.complete()
|
||||
await this.gracefullyKillProcess()
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string> {
|
||||
if (!this.truePID) {
|
||||
return null
|
||||
@@ -187,7 +202,7 @@ export class SessionsService {
|
||||
) {
|
||||
nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
|
||||
this.logger = log.create('sessions')
|
||||
this.persistenceProviders = this.persistenceProviders.filter(x => x.isAvailable())
|
||||
this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
|
||||
}
|
||||
|
||||
async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
|
||||
|
@@ -27,14 +27,18 @@ export class TerminalService {
|
||||
|
||||
async reloadShells () {
|
||||
this.shells$ = new AsyncSubject<IShell[]>()
|
||||
let shellLists = await Promise.all(this.shellProviders.map(x => x.provide()))
|
||||
let shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
|
||||
this.shells$.next(shellLists.reduce((a, b) => a.concat(b)))
|
||||
this.shells$.complete()
|
||||
}
|
||||
|
||||
async openTab (shell?: IShell, cwd?: string): Promise<TerminalTabComponent> {
|
||||
if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
if (!cwd) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
} else {
|
||||
cwd = this.config.store.terminal.workingDirectory || null
|
||||
}
|
||||
}
|
||||
if (!shell) {
|
||||
let shells = await this.shells$.toPromise()
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider, ComponentType } from 'terminus-settings'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class TerminalSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'terminal'
|
||||
title = 'Terminal'
|
||||
|
||||
getComponentType (): ComponentType {
|
||||
getComponentType (): any {
|
||||
return TerminalSettingsTabComponent
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,14 @@ export class WindowsStockShellsProvider extends ShellProvider {
|
||||
]
|
||||
},
|
||||
{ id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
|
||||
{ id: 'powershell', name: 'PowerShell', command: 'powershell.exe' },
|
||||
{
|
||||
id: 'powershell',
|
||||
name: 'PowerShell',
|
||||
command: 'powershell.exe',
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,10 @@ export class WSLShellProvider extends ShellProvider {
|
||||
return [{
|
||||
id: 'wsl',
|
||||
name: 'Bash on Windows',
|
||||
command: wslPath
|
||||
command: wslPath,
|
||||
env: {
|
||||
TERM: 'xterm-color',
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,5 @@ module.exports = [
|
||||
require('./terminus-terminal/webpack.config.js'),
|
||||
require('./terminus-community-color-schemes/webpack.config.js'),
|
||||
require('./terminus-plugin-manager/webpack.config.js'),
|
||||
require('./terminus-ssh/webpack.config.js'),
|
||||
]
|
||||
|
Reference in New Issue
Block a user