Compare commits
169 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c7bee48199 | ||
![]() |
1326fd7355 | ||
![]() |
ee368a3716 | ||
![]() |
c9367d04c3 | ||
![]() |
2f63a1ebbb | ||
![]() |
c956ee1183 | ||
![]() |
f76a5505ec | ||
![]() |
4aafcf82e8 | ||
![]() |
76ffef751c | ||
![]() |
5c22e22caa | ||
![]() |
c98fd2042d | ||
![]() |
ac45f24bf9 | ||
![]() |
60c85c49cc | ||
![]() |
2a1b5d6a30 | ||
![]() |
931335ec31 | ||
![]() |
f3ddb54499 | ||
![]() |
275f9e5633 | ||
![]() |
b988c36bf9 | ||
![]() |
381ee6f1b2 | ||
![]() |
a423447532 | ||
![]() |
bb386f7f8a | ||
![]() |
99a86e7ff5 | ||
![]() |
2e9d8f609e | ||
![]() |
71e6caff0a | ||
![]() |
235a53018e | ||
![]() |
dd22aba099 | ||
![]() |
2db700a77f | ||
![]() |
f5fdf6cf44 | ||
![]() |
8f02239815 | ||
![]() |
e462e207de | ||
![]() |
24210dee38 | ||
![]() |
87a98288b9 | ||
![]() |
be2015a833 | ||
![]() |
75a59769fc | ||
![]() |
c28b474f24 | ||
![]() |
e9ea8b0506 | ||
![]() |
44367465cc | ||
![]() |
c017c42b70 | ||
![]() |
26117dfce0 | ||
![]() |
ae809e9ee2 | ||
![]() |
58516718d6 | ||
![]() |
9dac5cdf9d | ||
![]() |
dbc291ff8c | ||
![]() |
dc5f7991b8 | ||
![]() |
df2e60150c | ||
![]() |
516572f9d9 | ||
![]() |
b3cca4f789 | ||
![]() |
e59ebb76ac | ||
![]() |
0476ef35bb | ||
![]() |
8b3139719a | ||
![]() |
66ebc0dd71 | ||
![]() |
11328d5880 | ||
![]() |
af2fd17c34 | ||
![]() |
d21d019b5c | ||
![]() |
f4a825f67b | ||
![]() |
4ce916241a | ||
![]() |
4780a6b913 | ||
![]() |
58710cfdbe | ||
![]() |
8a6b3a6fbb | ||
![]() |
40897c278b | ||
![]() |
d54dbc3c50 | ||
![]() |
789ca2c635 | ||
![]() |
eacc30bc82 | ||
![]() |
dc7fc30d27 | ||
![]() |
6de6fd8f40 | ||
![]() |
fb9fb57a18 | ||
![]() |
e434fe5a8a | ||
![]() |
1b1127ab28 | ||
![]() |
fe8dd891b4 | ||
![]() |
f7b603a631 | ||
![]() |
44040ba54b | ||
![]() |
f87efcf5bd | ||
![]() |
e2d467046b | ||
![]() |
220ae6ccaa | ||
![]() |
3c6374be19 | ||
![]() |
d32e31d45e | ||
![]() |
d7a33dc0ce | ||
![]() |
bc736dd13a | ||
![]() |
7cf8f8d58e | ||
![]() |
ff0cd36b6a | ||
![]() |
767bc8e56f | ||
![]() |
8801839c7a | ||
![]() |
642293dbdb | ||
![]() |
99e3144ddf | ||
![]() |
c38b684b56 | ||
![]() |
f8871cb1c5 | ||
![]() |
7ab3238617 | ||
![]() |
6d89d7a8d0 | ||
![]() |
e277c52f71 | ||
![]() |
2cafd97751 | ||
![]() |
ad78f38210 | ||
![]() |
a647b394e5 | ||
![]() |
4ec4620cd5 | ||
![]() |
4f32908c48 | ||
![]() |
682b336784 | ||
![]() |
0687e31d21 | ||
![]() |
8f0ba06f63 | ||
![]() |
d221ad3078 | ||
![]() |
5b4d5efcc9 | ||
![]() |
8987cc39aa | ||
![]() |
2f2dd442ad | ||
![]() |
2e7de08649 | ||
![]() |
88f4c7e835 | ||
![]() |
de3218522e | ||
![]() |
f7ed97b8a4 | ||
![]() |
8bfc86e623 | ||
![]() |
50b1fb2410 | ||
![]() |
327daf03ef | ||
![]() |
224a72d029 | ||
![]() |
3c5615e464 | ||
![]() |
e99908761d | ||
![]() |
0ff81abb0c | ||
![]() |
b4e703674b | ||
![]() |
dc53668685 | ||
![]() |
2df848f4c0 | ||
![]() |
247cf6f93e | ||
![]() |
797265abb1 | ||
![]() |
62bdcb1af4 | ||
![]() |
25ae56718d | ||
![]() |
b24978dc0e | ||
![]() |
46a4df108b | ||
![]() |
3091e2be69 | ||
![]() |
9a49e94642 | ||
![]() |
cf13fca835 | ||
![]() |
174a1bcca7 | ||
![]() |
80c781a8ca | ||
![]() |
d71ee6b6f1 | ||
![]() |
3a7204c2ee | ||
![]() |
5236956469 | ||
![]() |
4dfb8df7fa | ||
![]() |
61e65c7ec8 | ||
![]() |
64ab172b8e | ||
![]() |
bd46b08c9d | ||
![]() |
4a97cc4383 | ||
![]() |
2c11feed83 | ||
![]() |
481aa654a3 | ||
![]() |
7d1ec5b869 | ||
![]() |
5f098ef791 | ||
![]() |
15029066e4 | ||
![]() |
dbf90a5ce3 | ||
![]() |
c6d4eb7083 | ||
![]() |
e56fac03b9 | ||
![]() |
1132a18a0a | ||
![]() |
ebc8d846e3 | ||
![]() |
123f68d705 | ||
![]() |
8f9a7539c4 | ||
![]() |
98d46468e5 | ||
![]() |
a6d5a93e6e | ||
![]() |
558ef9894c | ||
![]() |
7357dc178a | ||
![]() |
cd7df7c700 | ||
![]() |
a4c4c93bac | ||
![]() |
a97619a474 | ||
![]() |
cd1cac96e9 | ||
![]() |
9861766da0 | ||
![]() |
bcd5f3b8bf | ||
![]() |
3e4f2c467d | ||
![]() |
e84270609f | ||
![]() |
62b1538462 | ||
![]() |
d6a62344b8 | ||
![]() |
d2e16cd73a | ||
![]() |
87d3b08c37 | ||
![]() |
3a7db137c5 | ||
![]() |
da0f4e7afc | ||
![]() |
e1980a6611 | ||
![]() |
08cc19946f | ||
![]() |
2f7d29c523 | ||
![]() |
67190e9622 | ||
![]() |
f42c3cdde4 |
@@ -370,6 +370,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "starxg",
|
||||
"name": "starxg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34997494?v=4",
|
||||
"profile": "https://github.com/starxg",
|
||||
"contributions": [
|
||||
"plugin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
56
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/app"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-core"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-settings"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-terminal"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-community-color-schemes"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-plugin-manager"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: npm
|
||||
directory: "/terminus-ssh"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 20
|
2
.github/workflows/docs.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 15
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
17
.github/workflows/linux.yml
vendored
@@ -2,7 +2,7 @@ name: Linux Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -15,6 +15,7 @@ jobs:
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo apt-get install bsdtar
|
||||
npm i -g yarn@1.19.1
|
||||
cd app
|
||||
yarn
|
||||
@@ -57,6 +58,8 @@ jobs:
|
||||
mv dist/*.deb artifact-deb/ || true
|
||||
mkdir artifact-rpm
|
||||
mv dist/*.rpm artifact-rpm/ || true
|
||||
mkdir artifact-pacman
|
||||
mv dist/*.pacman artifact-pacman/ || true
|
||||
mkdir artifact-snap
|
||||
mv dist/*.snap artifact-snap/ || true
|
||||
mkdir artifact-tar.gz
|
||||
@@ -65,19 +68,25 @@ jobs:
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload DEB
|
||||
with:
|
||||
name: Linux .deb
|
||||
name: Linux DEB
|
||||
path: artifact-deb
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload RPM
|
||||
with:
|
||||
name: Linux .rpm
|
||||
name: Linux RPM
|
||||
path: artifact-rpm
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Pacman Package
|
||||
with:
|
||||
name: Linux Pacman
|
||||
path: artifact-pacman
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Snap
|
||||
with:
|
||||
name: Linux .snap
|
||||
name: Linux Snap
|
||||
path: artifact-snap
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
|
1
.gitignore
vendored
@@ -33,3 +33,4 @@ sentry.properties
|
||||
sentry-symbols.js
|
||||
|
||||
terminus-ssh/util/pagent.exe
|
||||
*.psd
|
||||
|
91
README.md
@@ -1,44 +1,92 @@
|
||||

|
||||

|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/eugeny/terminus.svg?label=License&style=flat-square"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus"><img alt="AppVeyor" src="https://img.shields.io/appveyor/ci/eugeny/****terminus****.svg?label=CI&logo=appveyor&logoColor=white&style=flat-square"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Eugeny/terminus/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/terminus/total.svg?label=RELEASE&logo=github&style=for-the-badge"></a> <a href="https://nightly.link/Eugeny/terminus/workflows/windows/master"><img src="https://shields.io/badge/-Nightly-blue?logo=windows&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/terminus/workflows/macos/master"><img src="https://shields.io/badge/-Nightly-black?logo=apple&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/terminus/workflows/linux/master"><img src="https://shields.io/badge/-Nightly-orange?logo=linux&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=magenta&logo=gitter&style=for-the-badge"></a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
|
||||
**Terminus** is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
||||
|
||||
* Integrated SSH client and connection manager
|
||||
* Theming and color schemes
|
||||
* Fully configurable shortcuts
|
||||
* Split panes
|
||||
* Remembers your tabs
|
||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
||||
* Direct file transfer from/to SSH sessions via Zmodem
|
||||
* Full Unicode support including double-width characters
|
||||
* Doesn't choke on fast-flowing outputs
|
||||
* Proper shell experience on Windows including tab completion (via Clink)
|
||||
* Integrated SSH client and connection manager
|
||||
* Integrated serial terminal
|
||||
* Theming and color schemes
|
||||
* Fully configurable shortcuts and multi-chord shortcuts
|
||||
* Split panes
|
||||
* Remembers your tabs
|
||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
||||
* Direct file transfer from/to SSH sessions via Zmodem
|
||||
* Full Unicode support including double-width characters
|
||||
* Doesn't choke on fast-flowing outputs
|
||||
* Proper shell experience on Windows including tab completion (via Clink)
|
||||
|
||||
|
||||
[](https://ko-fi.com/eugeny)
|
||||
|
||||
---
|
||||
|
||||
# Contents
|
||||
|
||||
- [Contents](#contents)
|
||||
- [What Terminus is and isn't](#what-terminus-is-and-isnt)
|
||||
- [Terminal features](#terminal-features)
|
||||
- [SSH Client](#ssh-client)
|
||||
- [Serial Terminal](#serial-terminal)
|
||||
- [Portable](#portable)
|
||||
- [Plugins](#plugins)
|
||||
- [Themes](#themes)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
<a name="about"></a>
|
||||
# What Terminus is and isn't
|
||||
|
||||
* **Terminus is** an alternative to Windows' standard terminal (conhost), PowerShell ISE, PuTTY or iTerm
|
||||
|
||||
* **Terminus is not** a new shell or a MinGW or Cygwin replacement. Neither is it lightweight - if RAM usage is of importance, consider [Conemu](https://conemu.github.io) or [Alacritty](https://github.com/jwilm/alacritty)
|
||||
|
||||
---
|
||||
<a name="terminal"></a>
|
||||
# Terminal features
|
||||
|
||||

|
||||
|
||||
* A V220 terminal + various extensions
|
||||
* Multiple nested split panes
|
||||
* Tabs on any side of the window
|
||||
* Optional dockable window with a global spawn hotkey ("Quake console")
|
||||
* Progress detection
|
||||
* Notification on process completion
|
||||
* Bracketed paste, multiline paste warnings
|
||||
* Font ligatures
|
||||
* Custom shell profiles
|
||||
* Optional RMB paste and copy-on select (PuTTY style)
|
||||
|
||||
<a name="ssh"></a>
|
||||
# SSH Client
|
||||
|
||||

|
||||
|
||||
* SSH2 client with a connection manager
|
||||
* X11 and port forwarding
|
||||
* Automatic jump host management
|
||||
* Agent forwarding (incl. Pageant and Windows native OpenSSH Agent)
|
||||
* Login scripts
|
||||
|
||||
<a name="serial"></a>
|
||||
# Serial Terminal
|
||||
|
||||
* Saved connections
|
||||
* Readline input support
|
||||
* Optional hex byte-by-byte input and hexdump output
|
||||
* Newline conversion
|
||||
* Automatic reconnection
|
||||
|
||||
<a name="portable"></a>
|
||||
# Portable
|
||||
|
||||
For portable in windows, user can create folder `data` at the same directory as `Terminal.exe` to save the settings.
|
||||
Terminus will run as a portable app on Windows, if you create a `data` folder in the same location where `Terminus.exe` lives.
|
||||
|
||||
<a name="plugins"></a>
|
||||
# Plugins
|
||||
|
||||
Plugins and themes can be installed directly from the Settings view inside Terminus.
|
||||
@@ -48,7 +96,9 @@ Plugins and themes can be installed directly from the Settings view inside Termi
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
|
||||
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to hterm tabs
|
||||
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
|
||||
|
||||
<a name="themes"></a>
|
||||
# Themes
|
||||
|
||||
* [hype](https://github.com/Eugeny/terminus-theme-hype) - a Hyper inspired theme
|
||||
@@ -57,8 +107,7 @@ Plugins and themes can be installed directly from the Settings view inside Termi
|
||||
* [windows10](https://www.npmjs.com/package/terminus-theme-windows10)
|
||||
* [altair](https://github.com/yxuko/terminus-altair)
|
||||
|
||||
---
|
||||
|
||||
<a name="contributing"></a>
|
||||
# Contributing
|
||||
|
||||
Pull requests and plugins are welcome!
|
||||
@@ -66,6 +115,7 @@ Pull requests and plugins are welcome!
|
||||
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) and [API docs](http://ajenti.org/terminus-docs/) for information of how the project is laid out, and a very brief plugin development tutorial.
|
||||
|
||||
---
|
||||
<a name="contributors"></a>
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
@@ -124,6 +174,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="http://patalong.pl"><img src="https://avatars.githubusercontent.com/u/29167842?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Piotr Patalong</b></sub></a><br /><a href="#design-VectorKappa" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/clarkwang"><img src="https://avatars.githubusercontent.com/u/157076?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Clark Wang</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=clarkwang" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iamchating"><img src="https://avatars.githubusercontent.com/u/7088153?v=4?s=100" width="100px;" alt=""/><br /><sub><b>iamchating</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=iamchating" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/starxg"><img src="https://avatars.githubusercontent.com/u/34997494?v=4?s=100" width="100px;" alt=""/><br /><sub><b>starxg</b></sub></a><br /><a href="#plugin-starxg" title="Plugin/utility libraries">🔌</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@@ -1,55 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{opacity:0.16;fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{opacity:0.16;fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{opacity:0.15;fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="260.9675" y1="871.1813" x2="919.1845" y2="491.1596">
|
||||
<stop offset="0" style="stop-color:#669ABD"/>
|
||||
<stop offset="1" style="stop-color:#77DBDB"/>
|
||||
</linearGradient>
|
||||
<polygon class="st0" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="553.5051" y1="617.8278" x2="626.647" y2="744.5132">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st1" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="114.6631" y1="744.5275" x2="334.0905" y2="871.2141">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="260.9478" y1="744.5281" x2="187.8059" y2="871.2135">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st3" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="114.663" y1="237.793" x2="553.5026" y2="491.1571">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st4" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="370.6562" y1="301.1281" x2="297.5094" y2="427.8221">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st5" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="78.0912" y1="554.4979" x2="736.3375" y2="174.4593">
|
||||
<stop offset="0" style="stop-color:#CCECFF"/>
|
||||
<stop offset="1" style="stop-color:#9FECED"/>
|
||||
</linearGradient>
|
||||
<polygon class="st6" points="297.51,765.64 151.23,681.18 590.08,427.81 151.23,174.45 297.5,90 882.61,427.82 "/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 1024 1024" xml:space="preserve" style="enable-background:new 0 0 1024 1024"><style type="text/css">.st0{fill:url(#SVGID_1_)}.st1{opacity:.16;fill:url(#SVGID_2_)}.st2{fill:url(#SVGID_3_)}.st3{opacity:.16;fill:url(#SVGID_4_)}.st4{fill:url(#SVGID_5_)}.st5{opacity:.15;fill:url(#SVGID_6_)}.st6{fill:url(#SVGID_7_)}</style><g><linearGradient id="SVGID_1_" x1="260.967" x2="919.184" y1="871.181" y2="491.16" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#669abd"/><stop offset="1" style="stop-color:#77dbdb"/></linearGradient><polygon points="297.54 934.52 882.6 596.72 882.61 427.82 297.54 765.65" class="st0"/><linearGradient id="SVGID_2_" x1="553.505" x2="626.647" y1="617.828" y2="744.513" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="297.54 934.52 882.6 596.72 882.61 427.82 297.54 765.65" class="st1"/></g><g><linearGradient id="SVGID_3_" x1="114.663" x2="334.091" y1="744.528" y2="871.214" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#6a8fad"/><stop offset="1" style="stop-color:#669abd"/></linearGradient><polygon points="151.23 681.18 151.22 850.09 297.54 934.52 297.54 765.65" class="st2"/><linearGradient id="SVGID_4_" x1="260.948" x2="187.806" y1="744.528" y2="871.213" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="151.23 681.18 151.22 850.09 297.54 934.52 297.54 765.65" class="st3"/></g><g><linearGradient id="SVGID_5_" x1="114.663" x2="553.503" y1="237.793" y2="491.157" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#6a8fad"/><stop offset="1" style="stop-color:#669abd"/></linearGradient><polygon points="151.23 174.45 151.21 343.36 443.79 512.27 590.08 427.81" class="st4"/><linearGradient id="SVGID_6_" x1="370.656" x2="297.509" y1="301.128" y2="427.822" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="151.23 174.45 151.21 343.36 443.79 512.27 590.08 427.81" class="st5"/></g><linearGradient id="SVGID_7_" x1="78.091" x2="736.337" y1="554.498" y2="174.459" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#ccecff"/><stop offset="1" style="stop-color:#9feced"/></linearGradient><polygon points="297.51 765.64 151.23 681.18 590.08 427.81 151.23 174.45 297.5 90 882.61 427.82" class="st6"/></svg>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,14 +1,21 @@
|
||||
import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron'
|
||||
import * as promiseIpc from 'electron-promise-ipc'
|
||||
import * as remote from '@electron/remote/main'
|
||||
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
import { pluginManager } from './pluginManager'
|
||||
import { PTYManager } from './pty'
|
||||
|
||||
export class Application {
|
||||
private tray?: Tray
|
||||
private ptyManager = new PTYManager()
|
||||
private windows: Window[] = []
|
||||
|
||||
constructor () {
|
||||
remote.initialize()
|
||||
this.ptyManager.init(this)
|
||||
|
||||
ipcMain.on('app:config-change', (_event, config) => {
|
||||
this.broadcast('host:config-change', config)
|
||||
})
|
||||
@@ -40,6 +47,7 @@ export class Application {
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache')
|
||||
app.commandLine.appendSwitch('max-active-webgl-contexts', '9000')
|
||||
app.commandLine.appendSwitch('lang', 'EN')
|
||||
app.allowRendererProcessReuse = false
|
||||
|
||||
@@ -106,7 +114,7 @@ export class Application {
|
||||
}
|
||||
|
||||
enableTray (): void {
|
||||
if (this.tray) {
|
||||
if (this.tray || process.platform === 'linux') {
|
||||
return
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -131,6 +139,9 @@ export class Application {
|
||||
}
|
||||
|
||||
disableTray (): void {
|
||||
if (process.platform === 'linux') {
|
||||
return
|
||||
}
|
||||
this.tray?.destroy()
|
||||
this.tray = null
|
||||
}
|
||||
@@ -201,13 +212,8 @@ export class Application {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
},
|
||||
|
57
app/lib/bufferizedPTY.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/** @hidden */
|
||||
module.exports = function patchPTYModule (mod) {
|
||||
const oldSpawn = mod.spawn
|
||||
if (mod.patched) {
|
||||
return
|
||||
}
|
||||
mod.patched = true
|
||||
mod.spawn = (file, args, opt) => {
|
||||
let terminal = oldSpawn(file, args, opt)
|
||||
let timeout = null
|
||||
let buffer = Buffer.from('')
|
||||
let lastFlush = 0
|
||||
let nextTimeout = 0
|
||||
|
||||
// Minimum prebuffering window (ms) if the input is non-stop flowing
|
||||
const minWindow = 5
|
||||
|
||||
// Maximum buffering time (ms) until output must be flushed unconditionally
|
||||
const maxWindow = 100
|
||||
|
||||
function flush () {
|
||||
if (buffer.length) {
|
||||
terminal.emit('data-buffered', buffer)
|
||||
}
|
||||
lastFlush = Date.now()
|
||||
buffer = Buffer.from('')
|
||||
}
|
||||
|
||||
function reschedule () {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
nextTimeout = Date.now() + minWindow
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
flush()
|
||||
}, minWindow)
|
||||
}
|
||||
|
||||
terminal.on('data', data => {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data)
|
||||
}
|
||||
buffer = Buffer.concat([buffer, data])
|
||||
if (Date.now() - lastFlush > maxWindow) {
|
||||
// Taking too much time buffering, flush to keep things interactive
|
||||
flush()
|
||||
} else {
|
||||
if (Date.now() > nextTimeout - maxWindow / 10) {
|
||||
// Extend the window if it's expiring
|
||||
reschedule()
|
||||
}
|
||||
}
|
||||
})
|
||||
return terminal
|
||||
}
|
||||
}
|
@@ -1,12 +1,8 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as electron from 'electron'
|
||||
|
||||
let appPath: string | null = null
|
||||
try {
|
||||
appPath = path.dirname(require('electron').app.getPath('exe'))
|
||||
} catch {
|
||||
appPath = path.dirname(require('electron').remote.app.getPath('exe'))
|
||||
}
|
||||
const appPath = path.dirname(electron.app.getPath('exe'))
|
||||
|
||||
if (fs.existsSync(path.join(appPath, 'terminus-data'))) {
|
||||
fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data'))
|
||||
@@ -14,9 +10,5 @@ if (fs.existsSync(path.join(appPath, 'terminus-data'))) {
|
||||
const portableData = path.join(appPath, 'data')
|
||||
if (fs.existsSync(portableData)) {
|
||||
console.log('reset user data to ' + portableData)
|
||||
try {
|
||||
require('electron').app.setPath('userData', portableData)
|
||||
} catch {
|
||||
require('electron').remote.app.setPath('userData', portableData)
|
||||
}
|
||||
electron.app.setPath('userData', portableData)
|
||||
}
|
||||
|
154
app/lib/pty.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import * as nodePTY from '@terminus-term/node-pty'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ipcMain } from 'electron'
|
||||
import { Application } from './app'
|
||||
|
||||
class PTYDataQueue {
|
||||
private buffers: Buffer[] = []
|
||||
private delta = 0
|
||||
private maxChunk = 1024
|
||||
private maxDelta = 1024 * 50
|
||||
private flowPaused = false
|
||||
|
||||
constructor (private pty: nodePTY.IPty, private onData: (data: Buffer) => void) { }
|
||||
|
||||
push (data: Buffer) {
|
||||
this.buffers.push(data)
|
||||
this.maybeEmit()
|
||||
}
|
||||
|
||||
ack (length: number) {
|
||||
this.delta -= length
|
||||
this.maybeEmit()
|
||||
}
|
||||
|
||||
private maybeEmit () {
|
||||
if (this.delta <= this.maxDelta && this.flowPaused) {
|
||||
this.resume()
|
||||
return
|
||||
}
|
||||
if (this.buffers.length > 0) {
|
||||
if (this.delta > this.maxDelta && !this.flowPaused) {
|
||||
this.pause()
|
||||
return
|
||||
}
|
||||
|
||||
const buffersToSend = []
|
||||
let totalLength = 0
|
||||
while (totalLength < this.maxChunk && this.buffers.length) {
|
||||
totalLength += this.buffers[0].length
|
||||
buffersToSend.push(this.buffers.shift())
|
||||
}
|
||||
|
||||
if (buffersToSend.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let toSend = Buffer.concat(buffersToSend)
|
||||
if (toSend.length > this.maxChunk) {
|
||||
this.buffers.unshift(toSend.slice(this.maxChunk))
|
||||
toSend = toSend.slice(0, this.maxChunk)
|
||||
}
|
||||
this.onData(toSend)
|
||||
this.delta += toSend.length
|
||||
|
||||
if (this.buffers.length) {
|
||||
setImmediate(() => this.maybeEmit())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private pause () {
|
||||
this.pty.pause()
|
||||
this.flowPaused = true
|
||||
}
|
||||
|
||||
private resume () {
|
||||
this.pty.resume()
|
||||
this.flowPaused = false
|
||||
this.maybeEmit()
|
||||
}
|
||||
}
|
||||
|
||||
export class PTY {
|
||||
private pty: nodePTY.IPty
|
||||
private outputQueue: PTYDataQueue
|
||||
|
||||
constructor (private id: string, private app: Application, ...args: any[]) {
|
||||
this.pty = (nodePTY as any).spawn(...args)
|
||||
for (const key of ['close', 'exit']) {
|
||||
(this.pty as any).on(key, (...eventArgs) => this.emit(key, ...eventArgs))
|
||||
}
|
||||
|
||||
this.outputQueue = new PTYDataQueue(this.pty, data => {
|
||||
setImmediate(() => this.emit('data-buffered', data))
|
||||
})
|
||||
|
||||
this.pty.on('data', data => this.outputQueue.push(Buffer.from(data)))
|
||||
}
|
||||
|
||||
getPID (): number {
|
||||
return this.pty.pid
|
||||
}
|
||||
|
||||
resize (columns: number, rows: number): void {
|
||||
if ((this.pty as any)._writable) {
|
||||
this.pty.resize(columns, rows)
|
||||
}
|
||||
}
|
||||
|
||||
write (buffer: Buffer): void {
|
||||
if ((this.pty as any)._writable) {
|
||||
this.pty.write(buffer.toString())
|
||||
}
|
||||
}
|
||||
|
||||
ackData (length: number): void {
|
||||
this.outputQueue.ack(length)
|
||||
}
|
||||
|
||||
kill (signal?: string): void {
|
||||
this.pty.kill(signal)
|
||||
}
|
||||
|
||||
private emit (event: string, ...args: any[]) {
|
||||
this.app.broadcast(`pty:${this.id}:${event}`, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
export class PTYManager {
|
||||
private ptys: Record<string, PTY|undefined> = {}
|
||||
|
||||
init (app: Application): void {
|
||||
//require('./bufferizedPTY')(nodePTY) // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
ipcMain.on('pty:spawn', (event, ...options) => {
|
||||
const id = uuidv4().toString()
|
||||
event.returnValue = id
|
||||
this.ptys[id] = new PTY(id, app, ...options)
|
||||
})
|
||||
|
||||
ipcMain.on('pty:exists', (event, id) => {
|
||||
event.returnValue = !!this.ptys[id]
|
||||
})
|
||||
|
||||
ipcMain.on('pty:get-pid', (event, id) => {
|
||||
event.returnValue = this.ptys[id]?.getPID()
|
||||
})
|
||||
|
||||
ipcMain.on('pty:resize', (_event, id, columns, rows) => {
|
||||
this.ptys[id]?.resize(columns, rows)
|
||||
})
|
||||
|
||||
ipcMain.on('pty:write', (_event, id, data) => {
|
||||
this.ptys[id]?.write(Buffer.from(data))
|
||||
})
|
||||
|
||||
ipcMain.on('pty:kill', (_event, id, signal) => {
|
||||
this.ptys[id]?.kill(signal)
|
||||
})
|
||||
|
||||
ipcMain.on('pty:ack-data', (_event, id, length) => {
|
||||
this.ptys[id]?.ackData(length)
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,16 +1,14 @@
|
||||
const { init } = String(process.type) === 'main' ? require('@sentry/electron/dist/main') : require('@sentry/electron/dist/renderer')
|
||||
import * as isDev from 'electron-is-dev'
|
||||
|
||||
|
||||
const SENTRY_DSN = 'https://4717a0a7ee0b4429bd3a0f06c3d7eec3@sentry.io/181876'
|
||||
let release = null
|
||||
try {
|
||||
release = require('electron').app.getVersion()
|
||||
} catch {
|
||||
release = require('electron').remote.app.getVersion()
|
||||
release = require('@electron/remote').app.getVersion()
|
||||
}
|
||||
|
||||
if (!isDev) {
|
||||
if (!process.env.TERMINUS_DEV) {
|
||||
init({
|
||||
dsn: SENTRY_DSN,
|
||||
release,
|
||||
|
@@ -117,13 +117,16 @@ export class Window {
|
||||
})
|
||||
|
||||
this.window.on('blur', () => {
|
||||
if (this.configStore.appearance?.dockHideOnBlur) {
|
||||
if (this.configStore.appearance.dock !== 'off' && this.configStore.appearance?.dockHideOnBlur) {
|
||||
this.hide()
|
||||
}
|
||||
})
|
||||
|
||||
this.window.loadURL(`file://${app.getAppPath()}/dist/index.html?${this.window.id}`, { extraHeaders: 'pragma: no-cache\n' })
|
||||
|
||||
this.window.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
this.window.webContents.setZoomFactor(1)
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
this.window.setMenu(null)
|
||||
}
|
||||
|
@@ -21,23 +21,24 @@
|
||||
"@angular/forms": "^11.1.1",
|
||||
"@angular/platform-browser": "^11.1.1",
|
||||
"@angular/platform-browser-dynamic": "^11.1.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^6.1.0",
|
||||
"@electron/remote": "1.0.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
||||
"@terminus-term/node-pty": "0.10.0-terminus.3",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-is-dev": "1.2.0",
|
||||
"electron-promise-ipc": "^2.2.4",
|
||||
"fontmanager-redux": "1.0.0",
|
||||
"glasstron": "0.0.7",
|
||||
"js-yaml": "4.0.0",
|
||||
"keytar": "^7.4.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"keytar": "^7.6.0",
|
||||
"mz": "^2.7.0",
|
||||
"native-process-working-directory": "^1.0.2",
|
||||
"ngx-toastr": "^13.2.0",
|
||||
"ngx-toastr": "^13.2.1",
|
||||
"npm": "6",
|
||||
"path": "0.12.7",
|
||||
"rxjs": "^6.6.6",
|
||||
"yargs": "^15.4.1",
|
||||
"rxjs": "^6.6.7",
|
||||
"yargs": "^16.2.0",
|
||||
"zone.js": "^0.11.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -45,12 +46,12 @@
|
||||
"serialport": "^9.0.7",
|
||||
"windows-blurbehind": "^1.0.1",
|
||||
"windows-native-registry": "^3.0.0",
|
||||
"windows-process-tree": "^0.2.4"
|
||||
"windows-process-tree": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.32",
|
||||
"@types/node": "14.14.31",
|
||||
"node-abi": "^2.21.0",
|
||||
"@types/mz": "2.7.3",
|
||||
"@types/node": "14.14.35",
|
||||
"node-abi": "^2.26.0",
|
||||
"source-map-support": "^0.5.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -2,8 +2,6 @@ import 'zone.js'
|
||||
import 'core-js/proposals/reflect-metadata'
|
||||
import 'rxjs'
|
||||
|
||||
import * as isDev from 'electron-is-dev'
|
||||
|
||||
import './global.scss'
|
||||
import './toastr.scss'
|
||||
|
||||
@@ -23,7 +21,7 @@ if (process.platform === 'win32' && !('HOME' in process.env)) {
|
||||
process.env.HOME = `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
if (process.env.TERMINUS_DEV && !process.env.TERMINUS_FORCE_ANGULAR_PROD) {
|
||||
console.warn('Running in debug mode')
|
||||
} else {
|
||||
enableProdMode()
|
||||
@@ -39,7 +37,7 @@ async function bootstrap (plugins: PluginInfo[], safeMode = false): Promise<NgMo
|
||||
const module = getRootModule(pluginsModules)
|
||||
window['rootModule'] = module
|
||||
return platformBrowserDynamic().bootstrapModule(module).then(moduleRef => {
|
||||
if (isDev) {
|
||||
if (process.env.TERMINUS_DEV) {
|
||||
const applicationRef = moduleRef.injector.get(ApplicationRef)
|
||||
const componentRef = applicationRef.components[0]
|
||||
enableDebugTools(componentRef)
|
||||
|
@@ -17,6 +17,10 @@ body {
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
& > svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -104,3 +108,33 @@ ngb-typeahead-window {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.hover-reveal {
|
||||
opacity: 0;
|
||||
|
||||
.hover-reveal-parent:hover &,
|
||||
*:hover > &,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes terminalShakeFrames {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import * as remote from '@electron/remote'
|
||||
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
const nodeRequire = (global as any).require
|
||||
|
||||
@@ -15,13 +16,13 @@ function normalizePath (p: string): string {
|
||||
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
|
||||
if (process.env.TERMINUS_DEV) {
|
||||
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
|
||||
nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath()))
|
||||
}
|
||||
|
||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const userPluginsPath = path.join(
|
||||
require('electron').remote.app.getPath('userData'),
|
||||
remote.app.getPath('userData'),
|
||||
'plugins',
|
||||
)
|
||||
|
||||
|
@@ -7,6 +7,7 @@
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
animation: 0.5s ease-out fadeIn;
|
||||
background: radial-gradient(#3a66820a 0%, #000e17 30%, black 100%);
|
||||
|
||||
&>div {
|
||||
width: 200px;
|
||||
@@ -23,6 +24,7 @@
|
||||
transition: 1s ease-out width;
|
||||
background: #a1c5e4;
|
||||
height: 3px;
|
||||
box-shadow: 0 0 2px #ffffff1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +39,8 @@
|
||||
|
||||
|
||||
.terminus-logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: url('../assets/logo.svg');
|
||||
background-repeat: none;
|
||||
background-size: contain;
|
||||
@@ -51,7 +53,7 @@
|
||||
font-family: 'Source Sans Pro';
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 42px;
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
|
||||
sup {
|
||||
|
@@ -68,7 +68,6 @@ module.exports = {
|
||||
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
|
||||
child_process: 'commonjs child_process',
|
||||
electron: 'commonjs electron',
|
||||
'electron-is-dev': 'commonjs electron-is-dev',
|
||||
fs: 'commonjs fs',
|
||||
'ngx-toastr': 'commonjs ngx-toastr',
|
||||
module: 'commonjs module',
|
||||
|
@@ -1,9 +1,10 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
|
||||
module.exports = {
|
||||
name: 'terminus-main',
|
||||
target: 'node',
|
||||
target: 'electron-main',
|
||||
entry: {
|
||||
main: path.resolve(__dirname, 'lib/index.ts'),
|
||||
},
|
||||
@@ -33,19 +34,25 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
'any-promise': 'commonjs any-promise',
|
||||
electron: 'commonjs electron',
|
||||
'electron-config': 'commonjs electron-config',
|
||||
'electron-debug': 'commonjs electron-debug',
|
||||
'electron-promise-ipc': 'commonjs electron-promise-ipc',
|
||||
'electron-vibrancy': 'commonjs electron-vibrancy',
|
||||
fs: 'commonjs fs',
|
||||
glasstron: 'commonjs glasstron',
|
||||
mz: 'commonjs mz',
|
||||
npm: 'commonjs npm',
|
||||
'@terminus-term/node-pty': 'commonjs @terminus-term/node-pty',
|
||||
path: 'commonjs path',
|
||||
rxjs: 'commonjs rxjs',
|
||||
'rxjs/operators': 'commonjs rxjs/operators',
|
||||
util: 'commonjs util',
|
||||
'source-map-support': 'commonjs source-map-support',
|
||||
'windows-swca': 'commonjs windows-swca',
|
||||
'windows-blurbehind': 'commonjs windows-blurbehind',
|
||||
'yargs/yargs': 'commonjs yargs/yargs',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
@@ -53,6 +60,8 @@ module.exports = {
|
||||
'process.type': '"main"',
|
||||
}),
|
||||
],
|
||||
// Ignore warnings due to yarg's dynamic module loading
|
||||
ignoreWarnings: [/node_modules\/yargs/],
|
||||
}
|
||||
|
||||
if (process.env.BUNDLE_ANALYZER) {
|
||||
module.exports.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
221
app/yarn.lock
@@ -51,6 +51,11 @@
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@electron/remote@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.0.4.tgz#f1c8cf3560bab762b462bfae9991919cced8bc33"
|
||||
integrity sha512-kguDJRhL3ZynHrkbX8Tr7xoAzGsNgh4eqXkycXb6cgXbOgehGqkBVe+MnjSVMXz3QJykerGKPy28gqcM7AFGYw==
|
||||
|
||||
"@iarna/cli@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"
|
||||
@@ -60,10 +65,12 @@
|
||||
update-notifier "^2.2.0"
|
||||
yargs "^8.0.2"
|
||||
|
||||
"@ng-bootstrap/ng-bootstrap@^6.1.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-6.2.0.tgz#0506d612ca6002bd8fa398d006fa2641013e11d4"
|
||||
integrity sha512-wqwhnJFyEwvzWQJjXrEt+7oBTSvu2qPbdYvUFYhDVzOJLWB5M7YEhDAkMrfHQJ0pZNBMGr580FqYue+XiURY0Q==
|
||||
"@ng-bootstrap/ng-bootstrap@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-7.0.0.tgz#3bfa62eb52fdb891b1ce693ea11c39127e2d1ab7"
|
||||
integrity sha512-SxUaptGWJmCxM0d2Zy1mx7K7p/YBwGZ69NmmBQVY4BE6p5av0hWrVmv9rzzfBz0rhxU7RPZLor2Jpaoq8Xyl4w==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@serialport/binding-abstract@^9.0.7":
|
||||
version "9.0.7"
|
||||
@@ -143,17 +150,17 @@
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
"@types/mz@0.0.32":
|
||||
version "0.0.32"
|
||||
resolved "https://registry.npmjs.org/@types/mz/-/mz-0.0.32.tgz"
|
||||
integrity sha512-cy3yebKhrHuOcrJGkfwNHhpTXQLgmXSv1BX+4p32j+VUQ6aP2eJ5cL7OvGcAQx75fCTFaAIIAKewvqL+iwSd4g==
|
||||
"@types/mz@2.7.3":
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.3.tgz#e42a21e73f5f9340fe4a176981fafb1eb8cc6c12"
|
||||
integrity sha512-Zp1NUJ4Alh3gaun0a5rkF3DL7b2j1WB6rPPI5h+CJ98sQnxe9qwskClvupz/4bqChGR3L/BRhTjlaOwR+uiZJg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@14.14.31":
|
||||
version "14.14.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
|
||||
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
|
||||
"@types/node@*", "@types/node@14.14.35":
|
||||
version "14.14.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
|
||||
integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
|
||||
|
||||
JSONStream@^1.3.4, JSONStream@^1.3.5:
|
||||
version "1.3.5"
|
||||
@@ -250,9 +257,9 @@ ansistyles@~0.1.3:
|
||||
resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz"
|
||||
integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=
|
||||
|
||||
any-promise@^1.0.0:
|
||||
any-promise@^1.0.0, any-promise@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
|
||||
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
|
||||
|
||||
aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2:
|
||||
@@ -540,14 +547,14 @@ cliui@^5.0.0:
|
||||
strip-ansi "^5.2.0"
|
||||
wrap-ansi "^5.1.0"
|
||||
|
||||
cliui@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz"
|
||||
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone@^1.0.2:
|
||||
version "1.0.4"
|
||||
@@ -867,11 +874,6 @@ electron-is-accelerator@^0.1.0:
|
||||
resolved "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz"
|
||||
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
|
||||
|
||||
electron-is-dev@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.2.0.tgz#2e5cea0a1b3ccf1c86f577cee77363ef55deb05e"
|
||||
integrity sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==
|
||||
|
||||
electron-is-dev@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.1.0.tgz"
|
||||
@@ -989,6 +991,11 @@ es6-promisify@^5.0.0:
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
@@ -1061,14 +1068,6 @@ find-up@^3.0.0:
|
||||
dependencies:
|
||||
locate-path "^3.0.0"
|
||||
|
||||
find-up@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
|
||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||
dependencies:
|
||||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
||||
@@ -1196,7 +1195,7 @@ get-caller-file@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
|
||||
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
@@ -1319,9 +1318,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-cache-semantics@^3.8.1:
|
||||
version "3.8.1"
|
||||
@@ -1598,10 +1597,10 @@ isstream@~0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz"
|
||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||
|
||||
js-yaml@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
|
||||
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
|
||||
js-yaml@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -1660,10 +1659,10 @@ keyboardevents-areequal@^0.2.1:
|
||||
resolved "https://registry.npmjs.org/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz"
|
||||
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
|
||||
|
||||
keytar@^7.4.0:
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.4.0.tgz#0a508d64850ca05aa3ba4127818037d13ca3219f"
|
||||
integrity sha512-nELmc35YjSE4ZNSFaID/743CgDt/MdV4JLX7rRewAh9mKvU72RtF3uJMY0MdMpwdDYZhmD8FSdRCD1J97lEyVg==
|
||||
keytar@^7.6.0:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.6.0.tgz#498e796443cb543d31722099443f29d7b5c44100"
|
||||
integrity sha512-H3cvrTzWb11+iv0NOAnoNAPgEapVZnYLVHZQyxmh7jdmVfR/c0jNNFEZ6AI38W/4DeTGTaY66ZX4Z1SbfKPvCQ==
|
||||
dependencies:
|
||||
node-addon-api "^3.0.0"
|
||||
prebuild-install "^6.0.0"
|
||||
@@ -1847,13 +1846,6 @@ locate-path@^3.0.0:
|
||||
p-locate "^3.0.0"
|
||||
path-exists "^3.0.0"
|
||||
|
||||
locate-path@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
|
||||
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
|
||||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lock-verify@^2.0.2, lock-verify@^2.1.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.2.1.tgz#81107948c51ed16f97b96ff8b60675affb243fc1"
|
||||
@@ -2106,17 +2098,17 @@ native-process-working-directory@^1.0.2:
|
||||
dependencies:
|
||||
node-addon-api "^3.1.0"
|
||||
|
||||
ngx-toastr@^13.2.0:
|
||||
version "13.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-13.2.0.tgz#3ef047c977a7d0fb67fed9338b8f5add3c87b356"
|
||||
integrity sha512-XU+wACX5hxwOJ4BtPMAUExQmYbjfvH3C/R4vcC9QK/dX2Zw+2w9tS9m4W6TUFyR92xZ/tGLBtsqRdrDRn3fJCw==
|
||||
ngx-toastr@^13.2.1:
|
||||
version "13.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-13.2.1.tgz#531bc7da6a524a861e0409bd2749878b3b3f8a3f"
|
||||
integrity sha512-UAzp7/xWK9IXA2LsOmhpaaIGCqscvJokoQpBNpAMrjEkDeSlFf8PWQAuQY795KW0mJb3qF9UG/s23nsXfMYKmg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
node-abi@^2.20.0, node-abi@^2.21.0, node-abi@^2.7.0:
|
||||
version "2.21.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48"
|
||||
integrity sha512-smhrivuPqEM3H5LmnY3KU6HfYv0u4QklgAxfFyRNujKUzbUcYZ+Jc2EhukB9SRcD2VpqhxM7n/MIcp1Ua1/JMg==
|
||||
node-abi@^2.20.0, node-abi@^2.26.0, node-abi@^2.7.0:
|
||||
version "2.26.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
|
||||
integrity sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
@@ -2528,7 +2520,7 @@ p-limit@^1.1.0:
|
||||
dependencies:
|
||||
p-try "^1.0.0"
|
||||
|
||||
p-limit@^2.0.0, p-limit@^2.2.0:
|
||||
p-limit@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
|
||||
@@ -2549,13 +2541,6 @@ p-locate@^3.0.0:
|
||||
dependencies:
|
||||
p-limit "^2.0.0"
|
||||
|
||||
p-locate@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
|
||||
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
|
||||
dependencies:
|
||||
p-limit "^2.2.0"
|
||||
|
||||
p-try@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz"
|
||||
@@ -2633,11 +2618,6 @@ path-exists@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
||||
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
|
||||
@@ -2695,28 +2675,7 @@ pkg-up@^2.0.0:
|
||||
dependencies:
|
||||
find-up "^2.1.0"
|
||||
|
||||
prebuild-install@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.0.tgz"
|
||||
integrity sha512-h2ZJ1PXHKWZpp1caLw0oX9sagVpL2YTk+ZwInQbQ3QqNd4J03O6MpFNmMTJlkfgPENWqe5kP0WjQLqz5OjLfsw==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^2.7.0"
|
||||
noop-logger "^0.1.1"
|
||||
npmlog "^4.0.1"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^3.0.3"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
which-pm-runs "^1.0.0"
|
||||
|
||||
prebuild-install@^6.0.1:
|
||||
prebuild-install@^6.0.0, prebuild-install@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.0.1.tgz#5902172f7a40eb67305b96c2a695db32636ee26d"
|
||||
integrity sha512-7GOJrLuow8yeiyv75rmvZyeMGzl8mdEX5gY69d6a6bHWmiPevwqFw+tQavhK0EYMaSg3/KD24cWqeQv1EWsqDQ==
|
||||
@@ -3062,10 +3021,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rxjs@^6.6.6:
|
||||
version "6.6.6"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
|
||||
integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
|
||||
rxjs@^6.6.7:
|
||||
version "6.6.7"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
|
||||
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
@@ -3262,9 +3221,9 @@ sshpk@^1.7.0:
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
ssri@^6.0.0, ssri@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
||||
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
|
||||
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
|
||||
dependencies:
|
||||
figgy-pudding "^3.5.1"
|
||||
|
||||
@@ -3709,10 +3668,10 @@ windows-native-registry@^3.0.0:
|
||||
dependencies:
|
||||
node-addon-api "^3.0.0"
|
||||
|
||||
windows-process-tree@^0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.4.tgz#747af587b54cc6c996f2be0836cc8a8fd0dc038f"
|
||||
integrity sha512-9gag9AHm3Iin/4YC1EwoIfZlqW/rG2eV7rJZ4Fy5NnAMGdewmnwsie5Rz+CJo2vSolqzzfw7hPeu3oOdniNejg==
|
||||
windows-process-tree@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.0.tgz#cf0d9291b22fba2a7f5a687c8272866e28fbcafd"
|
||||
integrity sha512-0bKI4gcd5MOsOpn2TdStCSlnjThtH6BdHrocekY9qCgTqgEtdaUs0B5BaqyzF9jXoTSwz38NMdE1F55o4fgv9Q==
|
||||
dependencies:
|
||||
nan "^2.13.2"
|
||||
|
||||
@@ -3740,10 +3699,10 @@ wrap-ansi@^5.1.0:
|
||||
string-width "^3.0.0"
|
||||
strip-ansi "^5.0.0"
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
@@ -3781,15 +3740,20 @@ xtend@~4.0.1:
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
|
||||
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz"
|
||||
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
@@ -3808,13 +3772,10 @@ yargs-parser@^15.0.1:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^18.1.2:
|
||||
version "18.1.3"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz"
|
||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.7"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
|
||||
integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
|
||||
|
||||
yargs-parser@^7.0.0:
|
||||
version "7.0.0"
|
||||
@@ -3840,22 +3801,18 @@ yargs@^14.2.3:
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^15.0.1"
|
||||
|
||||
yargs@^15.4.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
yargs@^16.2.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
dependencies:
|
||||
cliui "^6.0.0"
|
||||
decamelize "^1.2.0"
|
||||
find-up "^4.1.0"
|
||||
get-caller-file "^2.0.1"
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^2.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^4.2.0"
|
||||
which-module "^2.0.0"
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^8.0.2:
|
||||
version "8.0.2"
|
||||
|
BIN
build/icons/Icon-MacOS-512x512@2x.png
Normal file
After Width: | Height: | Size: 166 KiB |
@@ -1,57 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.8;fill:#00232E;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{opacity:0.16;fill:url(#SVGID_2_);}
|
||||
.st3{fill:url(#SVGID_3_);}
|
||||
.st4{opacity:0.16;fill:url(#SVGID_4_);}
|
||||
.st5{fill:url(#SVGID_5_);}
|
||||
.st6{opacity:0.15;fill:url(#SVGID_6_);}
|
||||
.st7{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<polygon class="st0" points="449.5,645.47 407.51,621.23 533.47,548.5 407.51,475.77 449.5,451.53 617.45,548.5 "/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="439.0065" y1="603.0394" x2="627.9464" y2="493.9549">
|
||||
<stop offset="0" style="stop-color:#669ABD"/>
|
||||
<stop offset="1" style="stop-color:#77DBDB"/>
|
||||
</linearGradient>
|
||||
<polygon class="st1" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="522.9788" y1="530.3148" x2="543.9741" y2="566.6795">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="566.6837" x2="459.9963" y2="603.0487">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st3" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="439.0009" y1="566.6838" x2="418.0056" y2="603.0486">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st4" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="421.2265" x2="522.9781" y2="493.9542">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st5" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="470.4924" y1="439.4067" x2="449.4958" y2="475.774">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st6" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="386.5123" y1="512.136" x2="575.4605" y2="403.0467">
|
||||
<stop offset="0" style="stop-color:#CCECFF"/>
|
||||
<stop offset="1" style="stop-color:#9FECED"/>
|
||||
</linearGradient>
|
||||
<polygon class="st7" points="449.5,572.74 407.51,548.5 533.48,475.77 407.51,403.04 449.49,378.8 617.45,475.77 "/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 1024 1024" xml:space="preserve" style="enable-background:new 0 0 1024 1024"><style type="text/css">.st0{opacity:.8;fill:#00232e}.st1{fill:url(#SVGID_1_)}.st2{opacity:.16;fill:url(#SVGID_2_)}.st3{fill:url(#SVGID_3_)}.st4{opacity:.16;fill:url(#SVGID_4_)}.st5{fill:url(#SVGID_5_)}.st6{opacity:.15;fill:url(#SVGID_6_)}.st7{fill:url(#SVGID_7_)}</style><polygon points="449.5 645.47 407.51 621.23 533.47 548.5 407.51 475.77 449.5 451.53 617.45 548.5" class="st0"/><g><linearGradient id="SVGID_1_" x1="439.007" x2="627.946" y1="603.039" y2="493.955" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#669abd"/><stop offset="1" style="stop-color:#77dbdb"/></linearGradient><polygon points="449.5 621.22 617.45 524.25 617.45 475.77 449.5 572.75" class="st1"/><linearGradient id="SVGID_2_" x1="522.979" x2="543.974" y1="530.315" y2="566.679" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="449.5 621.22 617.45 524.25 617.45 475.77 449.5 572.75" class="st2"/></g><g><linearGradient id="SVGID_3_" x1="397.01" x2="459.996" y1="566.684" y2="603.049" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#6a8fad"/><stop offset="1" style="stop-color:#669abd"/></linearGradient><polygon points="407.51 548.5 407.5 596.99 449.5 621.22 449.5 572.75" class="st3"/><linearGradient id="SVGID_4_" x1="439.001" x2="418.006" y1="566.684" y2="603.049" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="407.51 548.5 407.5 596.99 449.5 621.22 449.5 572.75" class="st4"/></g><g><linearGradient id="SVGID_5_" x1="397.01" x2="522.978" y1="421.226" y2="493.954" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#6a8fad"/><stop offset="1" style="stop-color:#669abd"/></linearGradient><polygon points="407.51 403.04 407.5 451.53 491.49 500.01 533.48 475.77" class="st5"/><linearGradient id="SVGID_6_" x1="470.492" x2="449.496" y1="439.407" y2="475.774" gradientUnits="userSpaceOnUse"><stop offset=".559" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000"/></linearGradient><polygon points="407.51 403.04 407.5 451.53 491.49 500.01 533.48 475.77" class="st6"/></g><linearGradient id="SVGID_7_" x1="386.512" x2="575.461" y1="512.136" y2="403.047" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#ccecff"/><stop offset="1" style="stop-color:#9feced"/></linearGradient><polygon points="449.5 572.74 407.51 548.5 533.48 475.77 407.51 403.04 449.49 378.8 617.45 475.77" class="st7"/></svg>
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
docs/readme-ssh.png
Normal file
After Width: | Height: | Size: 371 KiB |
BIN
docs/readme-terminal.png
Normal file
After Width: | Height: | Size: 728 KiB |
@@ -31,6 +31,8 @@ files:
|
||||
extraResources:
|
||||
- builtin-plugins
|
||||
- extras
|
||||
asarUnpack:
|
||||
- dist/*.map
|
||||
publish:
|
||||
- provider: github
|
||||
|
||||
@@ -68,6 +70,8 @@ linux:
|
||||
artifactName: terminus-${version}-linux.${ext}
|
||||
executableArgs:
|
||||
- "--no-sandbox"
|
||||
desktop:
|
||||
StartupWMClass: terminus
|
||||
snap:
|
||||
plugs:
|
||||
- default
|
||||
@@ -84,9 +88,12 @@ deb:
|
||||
- libxtst6
|
||||
- libnss3
|
||||
afterInstall: build/linux/after-install.tpl
|
||||
pacman:
|
||||
depends:
|
||||
- gnome-keyring
|
||||
- libsecret
|
||||
rpm:
|
||||
depends:
|
||||
- screen
|
||||
- gnome-keyring
|
||||
fpm:
|
||||
- '--rpm-rpmbuild-define'
|
||||
|
35
package.json
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.2",
|
||||
"@sentry/cli": "^1.63.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@sentry/cli": "^1.64.2",
|
||||
"@sentry/electron": "^2.4.0",
|
||||
"@terminus-term/to-string-loader": "1.1.7-beta.1",
|
||||
"@types/electron-config": "^3.2.2",
|
||||
"@types/electron-debug": "^2.1.0",
|
||||
"@types/fs-extra": "^8.1.1",
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/node": "14.14.31",
|
||||
"@types/node": "14.14.35",
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.1",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"apply-loader": "2.0.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"compare-versions": "^3.6.0",
|
||||
"core-js": "^3.9.1",
|
||||
"core-js": "^3.12.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "5.0.1",
|
||||
"electron": "12.0.1",
|
||||
"css-loader": "5.2.0",
|
||||
"electron": "12.0.6",
|
||||
"electron-builder": "22.10.5",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-installer-snap": "^5.1.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-rebuild": "^2.3.5",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-plugin-import": "^2.21.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"graceful-fs": "^4.2.6",
|
||||
@@ -32,12 +32,12 @@
|
||||
"json-loader": "0.5.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"macos-release": "^2.4.1",
|
||||
"node-abi": "^2.19.3",
|
||||
"node-gyp": "^7.1.2",
|
||||
"node-abi": "^2.21.0",
|
||||
"node-gyp": "^8.0.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"npmlog": "4.1.2",
|
||||
"npx": "^10.2.2",
|
||||
"patch-package": "^6.2.2",
|
||||
"patch-package": "^6.4.7",
|
||||
"pug": "^3.0.2",
|
||||
"pug-html-loader": "1.1.5",
|
||||
"pug-lint": "^2.6.0",
|
||||
@@ -52,12 +52,13 @@
|
||||
"style-loader": "^2.0.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"tslib": "^2.1.0",
|
||||
"typedoc": "^0.20.28",
|
||||
"typedoc": "^0.20.36",
|
||||
"typescript": "^3.9.9",
|
||||
"url-loader": "^4.1.1",
|
||||
"val-loader": "3.0.0",
|
||||
"webpack": "^5.18.0",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"val-loader": "3.1.0",
|
||||
"webpack": "^5.31.0",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-cli": "^4.6.0",
|
||||
"yaml-loader": "0.6.0"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -68,7 +69,7 @@
|
||||
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js && webpack --color --config terminus-serial/webpack.config.js",
|
||||
"build:typings": "node scripts/build-typings.js",
|
||||
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug --inspect",
|
||||
"start:prod": "electron app --debug",
|
||||
"prod": "cross-env TERMINUS_DEV=1 electron app",
|
||||
"docs": "typedoc --out docs/api --tsconfig terminus-core/src/tsconfig.typings.json terminus-core/src/index.ts && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src/index.ts && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src/index.ts",
|
||||
|
@@ -7,7 +7,7 @@ const isCI = !!process.env.GITHUB_REF
|
||||
|
||||
builder({
|
||||
dir: true,
|
||||
linux: ['deb', 'tar.gz', 'rpm'],
|
||||
linux: ['deb', 'tar.gz', 'rpm', 'pacman'],
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
|
@@ -17,11 +17,13 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron/remote": "1.0.4",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/winston": "^2.3.6",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.1.3",
|
||||
"clone-deep": "^4.0.1",
|
||||
"core-js": "^3.1.2",
|
||||
"deepmerge": "^4.1.1",
|
||||
"electron-updater": "^4.0.6",
|
||||
@@ -29,7 +31,7 @@
|
||||
"mixpanel": "^0.10.2",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^10.1.0",
|
||||
"readable-stream": "2.3.7",
|
||||
"readable-stream": "3.6.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.3.3"
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export { BaseComponent, SubscriptionContainer } from '../components/base.component'
|
||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||
export { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||
@@ -21,4 +22,5 @@ export { NotificationsService } from '../services/notifications.service'
|
||||
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { TabsService } from '../services/tabs.service'
|
||||
export { UpdaterService } from '../services/updater.service'
|
||||
export * from '../utils'
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import deepClone from 'clone-deep'
|
||||
import { TabComponentType } from '../services/tabs.service'
|
||||
|
||||
export interface RecoveredTab {
|
||||
@@ -35,10 +36,26 @@ export interface RecoveryToken {
|
||||
* ```
|
||||
*/
|
||||
export abstract class TabRecoveryProvider {
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[boolean]] whether this [[TabRecoveryProvider]] can recover a tab from this token
|
||||
*/
|
||||
abstract async applicableTo (recoveryToken: RecoveryToken): Promise<boolean>
|
||||
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs
|
||||
* or `null` if this token is from a different tab type or is not supported
|
||||
*/
|
||||
abstract async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null>
|
||||
abstract async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab>
|
||||
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[RecoveryToken]] a new recovery token to create the duplicate tab from
|
||||
*
|
||||
* The default implementation just returns a deep copy of the original token
|
||||
*/
|
||||
duplicate (recoveryToken: RecoveryToken): RecoveryToken {
|
||||
return deepClone(recoveryToken)
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,6 @@ title-bar(
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
[hasActivity]='tab.activity$|async',
|
||||
@animateTab,
|
||||
[@.disabled]='hasVerticalTabs()',
|
||||
(click)='app.selectTab(tab)',
|
||||
@@ -86,7 +85,7 @@ title-bar(
|
||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||
*ngIf='updatesAvailable',
|
||||
title='Update available - Click to install',
|
||||
(click)='updateApp()',
|
||||
(click)='updater.update()',
|
||||
[fastHtmlBind]='updateIcon'
|
||||
)
|
||||
|
||||
@@ -95,12 +94,14 @@ title-bar(
|
||||
&& (hostApp.platform == Platform.Windows || hostApp.platform == Platform.Linux)',
|
||||
)
|
||||
|
||||
start-page(*ngIf='ready && app.tabs.length == 0')
|
||||
.content
|
||||
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body(
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
)
|
||||
tab-body.content-tab(
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[class.content-tab-active]='tab == app.activeTab',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
)
|
||||
|
||||
ng-template(ngbModalContainer)
|
||||
|
@@ -134,9 +134,24 @@ $side-tab-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
> .content-tab {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
left: -1000%;
|
||||
|
||||
&.content-tab-active {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hotkey-hint {
|
||||
|
@@ -68,7 +68,6 @@ export class AppRootComponent {
|
||||
|
||||
private constructor (
|
||||
private docking: DockingService,
|
||||
private electron: ElectronService,
|
||||
private hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
private touchbar: TouchbarService,
|
||||
@@ -76,6 +75,7 @@ export class AppRootComponent {
|
||||
public config: ConfigService,
|
||||
public app: AppService,
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
electron: ElectronService,
|
||||
log: LogService,
|
||||
ngbModal: NgbModal,
|
||||
_themes: ThemesService,
|
||||
@@ -136,9 +136,13 @@ export class AppRootComponent {
|
||||
ngbModal.open(SafeModeModalComponent)
|
||||
}
|
||||
|
||||
this.updater.check().then(available => {
|
||||
this.updatesAvailable = available
|
||||
})
|
||||
setInterval(() => {
|
||||
if (this.config.store.enableAutomaticUpdates) {
|
||||
this.updater.check().then(available => {
|
||||
this.updatesAvailable = available
|
||||
})
|
||||
}
|
||||
}, 3600 * 12)
|
||||
|
||||
this.touchbar.update()
|
||||
|
||||
@@ -190,20 +194,6 @@ export class AppRootComponent {
|
||||
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
|
||||
}
|
||||
|
||||
async updateApp () {
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: 'Installing the update will close all tabs and restart Terminus.',
|
||||
buttons: ['Cancel', 'Update'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
this.updater.update()
|
||||
}
|
||||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
}
|
||||
|
54
terminus-core/src/components/base.component.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Observable, Subscription } from 'rxjs'
|
||||
|
||||
interface CancellableEvent {
|
||||
element: HTMLElement
|
||||
event: string
|
||||
handler: EventListenerOrEventListenerObject
|
||||
options?: boolean|AddEventListenerOptions
|
||||
}
|
||||
|
||||
export class SubscriptionContainer {
|
||||
private subscriptions: Subscription[] = []
|
||||
private events: CancellableEvent[] = []
|
||||
|
||||
addEventListener (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
|
||||
element.addEventListener(event, handler, options)
|
||||
this.events.push({
|
||||
element,
|
||||
event,
|
||||
handler,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
subscribe <T> (observable: Observable<T>, handler: (v: T) => void): void {
|
||||
this.subscriptions.push(observable.subscribe(handler))
|
||||
}
|
||||
|
||||
cancelAll (): void {
|
||||
for (const s of this.subscriptions) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
for (const e of this.events) {
|
||||
e.element.removeEventListener(e.event, e.handler, e.options)
|
||||
}
|
||||
this.subscriptions = []
|
||||
this.events = []
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseComponent {
|
||||
private subscriptionContainer = new SubscriptionContainer()
|
||||
|
||||
addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
|
||||
this.subscriptionContainer.addEventListener(element, event, handler, options)
|
||||
}
|
||||
|
||||
subscribeUntilDestroyed <T> (observable: Observable<T>, handler: (v: T) => void): void {
|
||||
this.subscriptionContainer.subscribe(observable, handler)
|
||||
}
|
||||
|
||||
ngOnDestroy (): void {
|
||||
this.subscriptionContainer.cancelAll()
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
import { RecoveryToken } from '../api/tabRecovery'
|
||||
import { BaseComponent } from './base.component'
|
||||
|
||||
/**
|
||||
* Represents an active "process" inside a tab,
|
||||
@@ -13,7 +14,7 @@ export interface BaseTabProcess {
|
||||
/**
|
||||
* Abstract base class for custom tab components
|
||||
*/
|
||||
export abstract class BaseTabComponent {
|
||||
export abstract class BaseTabComponent extends BaseComponent {
|
||||
/**
|
||||
* Parent tab (usually a SplitTabComponent)
|
||||
*/
|
||||
@@ -69,6 +70,7 @@ export abstract class BaseTabComponent {
|
||||
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||
|
||||
protected constructor () {
|
||||
super()
|
||||
this.focused$.subscribe(() => {
|
||||
this.hasFocus = true
|
||||
})
|
||||
@@ -158,10 +160,17 @@ export abstract class BaseTabComponent {
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
this.progress.complete()
|
||||
this.activity.complete()
|
||||
this.recoveryStateChangedHint.complete()
|
||||
if (!skipDestroyedEvent) {
|
||||
this.destroyed.next()
|
||||
}
|
||||
this.destroyed.complete()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
ngOnDestroy (): void {
|
||||
this.destroy()
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
}
|
||||
|
@@ -17,3 +17,8 @@
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core'
|
||||
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
|
||||
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
|
||||
@@ -163,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
/** @hidden */
|
||||
private focusedTab: BaseTabComponent|null = null
|
||||
private maximizedTab: BaseTabComponent|null = null
|
||||
private hotkeysSubscription: Subscription
|
||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||
|
||||
private tabAdded = new Subject<BaseTabComponent>()
|
||||
@@ -210,7 +209,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
})
|
||||
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus || !this.focusedTab) {
|
||||
return
|
||||
}
|
||||
@@ -256,7 +255,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
/** @hidden */
|
||||
async ngAfterViewInit (): Promise<void> {
|
||||
if (this._recoveredState) {
|
||||
await this.recoverContainer(this.root, this._recoveredState)
|
||||
await this.recoverContainer(this.root, this._recoveredState, this._recoveredState.duplicate)
|
||||
this.layout()
|
||||
setTimeout(() => {
|
||||
if (this.hasFocus) {
|
||||
@@ -272,7 +271,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
|
||||
/** @hidden */
|
||||
ngOnDestroy (): void {
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
this.tabAdded.complete()
|
||||
this.tabRemoved.complete()
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
/** @returns Flat list of all sub-tabs */
|
||||
@@ -497,15 +498,18 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
this.viewRefs.set(tab, ref)
|
||||
|
||||
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
|
||||
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
||||
|
||||
tab.titleChange$.subscribe(t => this.setTitle(t))
|
||||
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
|
||||
tab.progress$.subscribe(p => this.setProgress(p))
|
||||
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
|
||||
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
|
||||
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
|
||||
if (tab.title) {
|
||||
this.setTitle(tab.title)
|
||||
}
|
||||
tab.destroyed$.subscribe(() => {
|
||||
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
|
||||
this.recoveryStateChangedHint.next()
|
||||
})
|
||||
tab.subscribeUntilDestroyed(tab.destroyed$, () => {
|
||||
this.removeTab(tab)
|
||||
})
|
||||
}
|
||||
@@ -567,7 +571,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
})
|
||||
}
|
||||
|
||||
private async recoverContainer (root: SplitContainer, state: any) {
|
||||
private async recoverContainer (root: SplitContainer, state: any, duplicate = false) {
|
||||
const children: (SplitContainer | BaseTabComponent)[] = []
|
||||
root.orientation = state.orientation
|
||||
root.ratios = state.ratios
|
||||
@@ -575,10 +579,10 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
for (const childState of state.children) {
|
||||
if (childState.type === 'app:split-tab') {
|
||||
const child = new SplitContainer()
|
||||
await this.recoverContainer(child, childState)
|
||||
await this.recoverContainer(child, childState, duplicate)
|
||||
children.push(child)
|
||||
} else {
|
||||
const recovered = await this.tabRecovery.recoverTab(childState)
|
||||
const recovered = await this.tabRecovery.recoverTab(childState, duplicate)
|
||||
if (recovered) {
|
||||
const tab = this.tabsService.create(recovered.type, recovered.options)
|
||||
children.push(tab)
|
||||
@@ -599,13 +603,21 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
|
||||
if (recoveryToken.type === 'app:split-tab') {
|
||||
return {
|
||||
type: SplitTabComponent,
|
||||
options: { _recoveredState: recoveryToken },
|
||||
}
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:split-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
|
||||
return {
|
||||
type: SplitTabComponent,
|
||||
options: { _recoveredState: recoveryToken },
|
||||
}
|
||||
}
|
||||
|
||||
duplicate (recoveryToken: RecoveryToken): RecoveryToken {
|
||||
return {
|
||||
...recoveryToken,
|
||||
duplicate: true,
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,11 @@
|
||||
:host {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
|
||||
>* {
|
||||
flex: auto;
|
||||
}
|
||||
>* {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
> perfect-scrollbar {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
.colorbar([style.background-color]='tab.color', *ngIf='tab.color != null')
|
||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex',
|
||||
#handle,
|
||||
[style.background-color]='tab.color',
|
||||
) {{index + 1}}
|
||||
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
|
||||
.activity-indicator(*ngIf='tab.activity$|async')
|
||||
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex', #handle) {{index + 1}}
|
||||
.name(
|
||||
[title]='tab.customTitle || tab.title',
|
||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||
) {{tab.customTitle || tab.title}}
|
||||
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
||||
|
@@ -4,6 +4,8 @@ $tabs-height: 38px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
> * { cursor: pointer; }
|
||||
|
||||
flex: 1000 1 200px;
|
||||
width: 200px;
|
||||
padding: 0 10px;
|
||||
@@ -64,7 +66,7 @@ $tabs-height: 38px;
|
||||
$button-size: 26px;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
border-radius: $button-size / 2;
|
||||
border-radius: $button-size / 6;
|
||||
line-height: $button-size;
|
||||
align-self: center;
|
||||
|
||||
@@ -76,7 +78,7 @@ $tabs-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .name {
|
||||
&:hover .name:not(.no-hover) {
|
||||
-webkit-mask-image: linear-gradient(black 0 0), linear-gradient(to left, transparent 0%, black 100%);
|
||||
-webkit-mask-size: calc(100% - 60px) auto, 60px auto;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
@@ -101,8 +103,30 @@ $tabs-height: 38px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 5px;
|
||||
z-index: -1;
|
||||
height: 3px;
|
||||
z-index: 1;
|
||||
transition: 0.25s width;
|
||||
}
|
||||
|
||||
.colorbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.active .activity-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activity-indicator {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
bottom: 4px;
|
||||
height: 2px;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
@@ -11,6 +11,7 @@ import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { BaseComponent } from './base.component'
|
||||
|
||||
/** @hidden */
|
||||
export interface SortableComponentProxy {
|
||||
@@ -23,10 +24,9 @@ export interface SortableComponentProxy {
|
||||
template: require('./tabHeader.component.pug'),
|
||||
styles: [require('./tabHeader.component.scss')],
|
||||
})
|
||||
export class TabHeaderComponent {
|
||||
export class TabHeaderComponent extends BaseComponent {
|
||||
@Input() index: number
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() @HostBinding('class.has-activity') hasActivity: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() progress: number|null
|
||||
@ViewChild('handle') handle?: ElementRef
|
||||
@@ -38,10 +38,12 @@ export class TabHeaderComponent {
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private zone: NgZone,
|
||||
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
super()
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, (hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
if (hotkey === 'rename-tab') {
|
||||
this.showRenameTabModal()
|
||||
@@ -52,8 +54,10 @@ export class TabHeaderComponent {
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.tab.progress$.subscribe(progress => {
|
||||
this.progress = progress
|
||||
this.subscribeUntilDestroyed(this.tab.progress$, progress => {
|
||||
this.zone.run(() => {
|
||||
this.progress = progress
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,7 +109,7 @@ export class TabHeaderComponent {
|
||||
if ($event.which === 3) {
|
||||
$event.preventDefault()
|
||||
|
||||
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
|
||||
const contextMenu = this.electron.Menu.buildFromTemplate(await this.buildContextMenu())
|
||||
|
||||
contextMenu.popup({
|
||||
x: $event.pageX,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage, MessageBoxOptions } from 'electron'
|
||||
import * as remote from '@electron/remote'
|
||||
|
||||
export interface MessageBoxResponse {
|
||||
response: number
|
||||
@@ -17,30 +18,31 @@ export class ElectronService {
|
||||
nativeImage: typeof NativeImage
|
||||
screen: Screen
|
||||
remote: Remote
|
||||
process: any
|
||||
autoUpdater: AutoUpdater
|
||||
TouchBar: typeof TouchBar
|
||||
BrowserWindow: typeof BrowserWindow
|
||||
Menu: typeof Menu
|
||||
MenuItem: typeof MenuItem
|
||||
private electron: any
|
||||
|
||||
/** @hidden */
|
||||
private constructor () {
|
||||
this.electron = require('electron')
|
||||
this.remote = this.electron.remote
|
||||
this.app = this.remote.app
|
||||
this.screen = this.remote.screen
|
||||
this.dialog = this.remote.dialog
|
||||
this.shell = this.electron.shell
|
||||
this.clipboard = this.electron.clipboard
|
||||
this.ipcRenderer = this.electron.ipcRenderer
|
||||
this.globalShortcut = this.remote.globalShortcut
|
||||
this.nativeImage = this.remote.nativeImage
|
||||
this.autoUpdater = this.remote.autoUpdater
|
||||
this.TouchBar = this.remote.TouchBar
|
||||
this.BrowserWindow = this.remote.BrowserWindow
|
||||
this.Menu = this.remote.Menu
|
||||
this.MenuItem = this.remote.MenuItem
|
||||
const electron = require('electron')
|
||||
this.shell = electron.shell
|
||||
this.clipboard = electron.clipboard
|
||||
this.ipcRenderer = electron.ipcRenderer
|
||||
|
||||
this.process = remote.getGlobal('process')
|
||||
this.app = remote.app
|
||||
this.screen = remote.screen
|
||||
this.dialog = remote.dialog
|
||||
this.globalShortcut = remote.globalShortcut
|
||||
this.nativeImage = remote.nativeImage
|
||||
this.autoUpdater = remote.autoUpdater
|
||||
this.TouchBar = remote.TouchBar
|
||||
this.BrowserWindow = remote.BrowserWindow
|
||||
this.Menu = remote.Menu
|
||||
this.MenuItem = remote.MenuItem
|
||||
}
|
||||
|
||||
async showMessageBox (
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||
import { stringifyKeySequence } from './hotkeys.util'
|
||||
import { stringifyKeySequence, EventData } from './hotkeys.util'
|
||||
import { ConfigService } from './config.service'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { HostAppService } from './hostApp.service'
|
||||
@@ -14,10 +14,6 @@ export interface PartialHotkeyMatch {
|
||||
|
||||
const KEY_TIMEOUT = 2000
|
||||
|
||||
interface EventBufferEntry {
|
||||
event: KeyboardEvent
|
||||
time: number
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HotkeysService {
|
||||
@@ -32,7 +28,7 @@ export class HotkeysService {
|
||||
get hotkey$ (): Observable<string> { return this._hotkey }
|
||||
|
||||
private _hotkey = new Subject<string>()
|
||||
private currentKeystrokes: EventBufferEntry[] = []
|
||||
private currentKeystrokes: EventData[] = []
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||
|
||||
@@ -73,7 +69,16 @@ export class HotkeysService {
|
||||
*/
|
||||
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
|
||||
(nativeEvent as any).event = name
|
||||
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
|
||||
this.currentKeystrokes.push({
|
||||
ctrlKey: nativeEvent.ctrlKey,
|
||||
metaKey: nativeEvent.metaKey,
|
||||
altKey: nativeEvent.altKey,
|
||||
shiftKey: nativeEvent.shiftKey,
|
||||
code: nativeEvent.code,
|
||||
key: nativeEvent.key,
|
||||
eventName: name,
|
||||
time: performance.now(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +109,7 @@ export class HotkeysService {
|
||||
|
||||
getCurrentKeystrokes (): string[] {
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
|
||||
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
|
||||
return stringifyKeySequence(this.currentKeystrokes)
|
||||
}
|
||||
|
||||
getCurrentFullyMatchedHotkey (): string|null {
|
||||
|
@@ -10,13 +10,26 @@ export const altKeyName = {
|
||||
linux: 'Alt',
|
||||
}[process.platform]
|
||||
|
||||
export function stringifyKeySequence (events: KeyboardEvent[]): string[] {
|
||||
export interface EventData {
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
shiftKey: boolean
|
||||
key: string
|
||||
code: string
|
||||
eventName: string
|
||||
time: number
|
||||
}
|
||||
|
||||
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
|
||||
|
||||
export function stringifyKeySequence (events: EventData[]): string[] {
|
||||
const items: string[] = []
|
||||
events = events.slice()
|
||||
|
||||
while (events.length > 0) {
|
||||
const event = events.shift()!
|
||||
if ((event as any).event === 'keydown') {
|
||||
if (event.eventName === 'keydown') {
|
||||
const itemKeys: string[] = []
|
||||
if (event.ctrlKey) {
|
||||
itemKeys.push('Ctrl')
|
||||
@@ -37,23 +50,29 @@ export function stringifyKeySequence (events: KeyboardEvent[]): string[] {
|
||||
}
|
||||
|
||||
let key = event.code
|
||||
key = key.replace('Key', '')
|
||||
key = key.replace('Arrow', '')
|
||||
key = key.replace('Digit', '')
|
||||
key = {
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
Backslash: '\\',
|
||||
IntlBackslash: '\\',
|
||||
Backquote: '`',
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
}[key] || key
|
||||
if (REGEX_LATIN_KEYNAME.test(event.key)) {
|
||||
// Handle Dvorak etc via the reported "character" instead of the scancode
|
||||
key = event.key.toUpperCase()
|
||||
} else {
|
||||
key = key.replace('Key', '')
|
||||
key = key.replace('Arrow', '')
|
||||
key = key.replace('Digit', '')
|
||||
key = {
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
Backslash: '\\',
|
||||
IntlBackslash: '`',
|
||||
Backquote: '~', // Electron says it's the tilde
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
}[key] || key
|
||||
}
|
||||
|
||||
itemKeys.push(key)
|
||||
items.push(itemKeys.join('-'))
|
||||
}
|
||||
|
@@ -40,16 +40,20 @@ export class TabRecoveryService {
|
||||
return token
|
||||
}
|
||||
|
||||
async recoverTab (token: RecoveryToken): Promise<RecoveredTab|null> {
|
||||
async recoverTab (token: RecoveryToken, duplicate = false): Promise<RecoveredTab|null> {
|
||||
for (const provider of this.config.enabledServices(this.tabRecoveryProviders ?? [])) {
|
||||
try {
|
||||
const tab = await provider.recover(token)
|
||||
if (tab !== null) {
|
||||
tab.options = tab.options || {}
|
||||
tab.options.color = token.tabColor ?? null
|
||||
tab.options.title = token.tabTitle || ''
|
||||
return tab
|
||||
if (!await provider.applicableTo(token)) {
|
||||
continue
|
||||
}
|
||||
if (duplicate) {
|
||||
token = provider.duplicate(token)
|
||||
}
|
||||
const tab = await provider.recover(token)
|
||||
tab.options = tab.options || {}
|
||||
tab.options.color = token.tabColor ?? null
|
||||
tab.options.title = token.tabTitle || ''
|
||||
return tab
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ export class TabsService {
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
const dup = await this.tabRecovery.recoverTab(token)
|
||||
const dup = await this.tabRecovery.recoverTab(token, true)
|
||||
if (dup) {
|
||||
return this.create(dup.type, dup.options)
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { HostAppService } from './hostApp.service'
|
||||
|
||||
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
||||
|
||||
@@ -17,8 +18,9 @@ export class UpdaterService {
|
||||
|
||||
private constructor (
|
||||
log: LogService,
|
||||
config: ConfigService,
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
this.logger = log.create('updater')
|
||||
|
||||
@@ -58,10 +60,47 @@ export class UpdaterService {
|
||||
}
|
||||
|
||||
async check (): Promise<boolean> {
|
||||
if (!this.config.store.enableAutomaticUpdates) {
|
||||
return false
|
||||
}
|
||||
if (!this.electronUpdaterAvailable) {
|
||||
if (this.electronUpdaterAvailable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations, prefer-const
|
||||
let cancel
|
||||
const onNoUpdate = () => {
|
||||
cancel()
|
||||
resolve(false)
|
||||
}
|
||||
const onUpdate = () => {
|
||||
cancel()
|
||||
resolve(this.downloaded)
|
||||
}
|
||||
const onError = (err) => {
|
||||
cancel()
|
||||
reject(err)
|
||||
}
|
||||
cancel = () => {
|
||||
this.electron.autoUpdater.off('error', onError)
|
||||
this.electron.autoUpdater.off('update-not-available', onNoUpdate)
|
||||
this.electron.autoUpdater.off('update-available', onUpdate)
|
||||
}
|
||||
this.electron.autoUpdater.on('error', onError)
|
||||
this.electron.autoUpdater.on('update-not-available', onNoUpdate)
|
||||
this.electron.autoUpdater.on('update-available', onUpdate)
|
||||
try {
|
||||
this.electron.autoUpdater.checkForUpdates()
|
||||
} catch (e) {
|
||||
this.electronUpdaterAvailable = false
|
||||
this.logger.info('Electron updater unavailable, falling back', e)
|
||||
}
|
||||
})
|
||||
|
||||
this.electron.autoUpdater.on('update-available', () => {
|
||||
this.logger.info('Update available')
|
||||
})
|
||||
|
||||
this.electron.autoUpdater.once('update-not-available', () => {
|
||||
this.logger.info('No updates')
|
||||
})
|
||||
|
||||
} else {
|
||||
this.logger.debug('Checking for updates through fallback method.')
|
||||
const response = await axios.get(UPDATES_URL)
|
||||
const data = response.data
|
||||
@@ -81,8 +120,18 @@ export class UpdaterService {
|
||||
if (!this.electronUpdaterAvailable) {
|
||||
this.electron.shell.openExternal(this.updateURL)
|
||||
} else {
|
||||
await this.downloaded
|
||||
this.electron.autoUpdater.quitAndInstall()
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: 'Installing the update will close all tabs and restart Terminus.',
|
||||
buttons: ['Cancel', 'Update'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
await this.downloaded
|
||||
this.electron.autoUpdater.quitAndInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -108,7 +108,7 @@ window-controls {
|
||||
|
||||
button:hover {
|
||||
background: rgba($black, 0.125);
|
||||
|
||||
|
||||
svg {
|
||||
fill: $black;
|
||||
}
|
||||
@@ -132,8 +132,6 @@ body {
|
||||
app-root {
|
||||
&> .content {
|
||||
.tab-bar {
|
||||
height: 40px;
|
||||
|
||||
.btn-tab-bar {
|
||||
background: transparent;
|
||||
line-height: 42px;
|
||||
@@ -171,6 +169,10 @@ app-root {
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
.activity-indicator {
|
||||
background:rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $black;
|
||||
background: $content-bg;
|
||||
@@ -192,10 +194,6 @@ app-root {
|
||||
&.active {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
background: linear-gradient(to bottom, rgba(208, 0, 0, 0) 95%, #36beff 96%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,10 +208,6 @@ app-root {
|
||||
&.active {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
background: linear-gradient(to top, rgba(208, 0, 0, 0) 95%, #36beff 96%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,11 +224,10 @@ tab-body {
|
||||
background: $content-bg;
|
||||
}
|
||||
|
||||
settings-tab > ngb-tabset {
|
||||
border-right: 1px solid $body-bg;
|
||||
|
||||
settings-tab > .content {
|
||||
& > .nav {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-right: 1px solid $body-bg;
|
||||
|
||||
& > .nav-item > .nav-link {
|
||||
border: none;
|
||||
@@ -317,10 +310,6 @@ hotkey-input-modal {
|
||||
}
|
||||
}
|
||||
|
||||
ngb-tabset .tab-content {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
[ngbradiogroup] > label.active {
|
||||
background: $blue;
|
||||
}
|
||||
|
@@ -76,6 +76,10 @@ app-root {
|
||||
background: $green;
|
||||
}
|
||||
|
||||
.activity-indicator {
|
||||
background:rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: $content-bg;
|
||||
@@ -97,10 +101,6 @@ app-root {
|
||||
&.active {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
background: linear-gradient(to bottom, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +115,6 @@ app-root {
|
||||
&.active {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
background: linear-gradient(to top, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,30 +131,6 @@ tab-body {
|
||||
background: $content-bg;
|
||||
}
|
||||
|
||||
settings-tab > ngb-tabset {
|
||||
border-right: 1px solid $body-bg;
|
||||
|
||||
& > .nav {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
flex-shrink: 0;
|
||||
|
||||
& > .nav-item > .nav-link {
|
||||
border: none;
|
||||
padding: 10px 50px 10px 20px;
|
||||
font-size: 14px;
|
||||
border-radius: 0;
|
||||
|
||||
&:not(.active) {
|
||||
color: $body-color;
|
||||
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multi-hotkey-input {
|
||||
.item {
|
||||
background: $body-bg2;
|
||||
@@ -221,10 +193,6 @@ hotkey-input-modal {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
ngb-tabset .tab-content {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
[ngbradiogroup] > label.active {
|
||||
background: $blue;
|
||||
}
|
||||
@@ -271,10 +239,6 @@ ngb-tabset .tab-content {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, .1);
|
||||
|
||||
&:not(.combi) {
|
||||
padding: $list-group-item-padding-y $list-group-item-padding-x;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
@@ -321,7 +285,7 @@ checkbox i.on {
|
||||
}
|
||||
|
||||
search-panel {
|
||||
background: rgba(39, 49, 60, 0.65) !important;
|
||||
background: rgba(39, 49, 60, 0.95) !important;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -46,7 +46,6 @@ $body-color: #ccc;
|
||||
$body-bg: #131d27;
|
||||
$body-bg2: #20333e;
|
||||
|
||||
|
||||
$font-family-sans-serif: "Source Sans Pro";
|
||||
$font-family-monospace: "Source Code Pro";
|
||||
$font-size-base: 14rem / 16;
|
||||
@@ -55,6 +54,12 @@ $font-size-sm: .85rem;
|
||||
|
||||
$line-height-base: 1.6;
|
||||
|
||||
$border-radius: .4rem;
|
||||
$border-radius-lg: .6rem;
|
||||
$border-radius-sm: .2rem;
|
||||
|
||||
// -----
|
||||
|
||||
$headings-color: #ced9e2;
|
||||
$headings-font-weight: lighter;
|
||||
|
||||
|
@@ -11,15 +11,20 @@
|
||||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@types/js-yaml@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb"
|
||||
integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA==
|
||||
"@electron/remote@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.0.4.tgz#f1c8cf3560bab762b462bfae9991919cced8bc33"
|
||||
integrity sha512-kguDJRhL3ZynHrkbX8Tr7xoAzGsNgh4eqXkycXb6cgXbOgehGqkBVe+MnjSVMXz3QJykerGKPy28gqcM7AFGYw==
|
||||
|
||||
"@types/semver@^7.3.4":
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb"
|
||||
integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==
|
||||
"@types/js-yaml@^4.0.0":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.1.tgz#5544730b65a480b18ace6b6ce914e519cec2d43b"
|
||||
integrity sha512-xdOvNmXmrZqqPy3kuCQ+fz6wA0xU5pji9cd1nDrflWaAWtYLLGk5ykW0H6yg5TVyehHP1pfmuuSaZkhP+kspVA==
|
||||
|
||||
"@types/semver@^7.3.5":
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.5.tgz#74deebbbcb1e86634dbf10a5b5e8798626f5a597"
|
||||
integrity sha512-iotVxtCCsPLRAvxMFFgxL8HD2l4mAZ2Oin7/VJ2ooWO0VOK4EGOGmZWZn1uCq7RofR3I/1IOSjCHlFT71eVK0Q==
|
||||
|
||||
"@types/shell-escape@^0.2.0":
|
||||
version "0.2.0"
|
||||
@@ -50,11 +55,6 @@ async@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
@@ -67,14 +67,23 @@ bootstrap@^4.1.3:
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
|
||||
integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
|
||||
|
||||
builder-util-runtime@8.7.3:
|
||||
version "8.7.3"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.3.tgz#0aaafa52d25295c939496f62231ca9ff06c30e40"
|
||||
integrity sha512-1Q2ReBqFblimF5g/TLg2+0M5Xzv0Ih5LxJ/BMWXvEy/e6pQKeeEpbkPMGsN6OiQgkygaZo5VXCXIjOkOQG5EoQ==
|
||||
builder-util-runtime@8.7.5:
|
||||
version "8.7.5"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz#fbe59e274818885e0d2e358d5b7017c34ae6b0f5"
|
||||
integrity sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
sax "^1.2.4"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
|
||||
dependencies:
|
||||
is-plain-object "^2.0.4"
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
color-convert@^1.9.1:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@@ -122,9 +131,9 @@ colorspace@1.1.x:
|
||||
text-hex "1.0.x"
|
||||
|
||||
core-js@^3.1.2:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
|
||||
integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
|
||||
version "3.12.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112"
|
||||
integrity sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
@@ -151,17 +160,18 @@ deepmerge@^4.1.1:
|
||||
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||
|
||||
electron-updater@^4.0.6:
|
||||
version "4.3.8"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.8.tgz#94f1731682a756385726183e2b04b959cb319456"
|
||||
integrity sha512-/tB82Ogb2LqaXrUzAD8waJC+TZV52Pr0Znfj7w+i4D+jA2GgrKFI3Pxjp+36y9FcBMQz7kYsMHcB6c5zBJao+A==
|
||||
version "4.3.9"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.9.tgz#247c660bafad7c07935e1b81acd3e9a5fd733154"
|
||||
integrity sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==
|
||||
dependencies:
|
||||
"@types/semver" "^7.3.4"
|
||||
builder-util-runtime "8.7.3"
|
||||
fs-extra "^9.1.0"
|
||||
js-yaml "^4.0.0"
|
||||
"@types/semver" "^7.3.5"
|
||||
builder-util-runtime "8.7.5"
|
||||
fs-extra "^10.0.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.4"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isequal "^4.5.0"
|
||||
semver "^7.3.4"
|
||||
semver "^7.3.5"
|
||||
|
||||
enabled@2.0.x:
|
||||
version "2.0.0"
|
||||
@@ -200,12 +210,11 @@ follow-redirects@^1.10.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||
|
||||
fs-extra@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
||||
fs-extra@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
|
||||
integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==
|
||||
dependencies:
|
||||
at-least-node "^1.0.0"
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
@@ -233,6 +242,13 @@ is-arrayish@^0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-plain-object@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
|
||||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
|
||||
@@ -243,10 +259,15 @@ isarray@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
js-yaml@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
|
||||
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
|
||||
isobject@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
js-yaml@^4.0.0, js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -259,6 +280,11 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
kind-of@^6.0.2:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
|
||||
kuler@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
@@ -269,6 +295,11 @@ lazy-val@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65"
|
||||
integrity sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==
|
||||
|
||||
lodash.escaperegexp@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
@@ -310,9 +341,9 @@ ng2-dnd@^5.0.2:
|
||||
integrity sha512-5mWWBePwvEPsNd/HkdbD543Q9mPyJofL6zkNydl8/Ah3qrrvZT2DaEPbknY08OgkXpI2qUGksc01OzzVlRQ9dQ==
|
||||
|
||||
ngx-perfect-scrollbar@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-perfect-scrollbar/-/ngx-perfect-scrollbar-10.1.0.tgz#6f7e2d8c849e595077b1c71992b6b544d56084d7"
|
||||
integrity sha512-CQ4pthb+UOoccTh3dOVcmBJsUILpHNBsKMHatif6AB2jsvhH6y2O6elMaoslhQEFqpv1fJlrU25AKIUJQZIA4A==
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ngx-perfect-scrollbar/-/ngx-perfect-scrollbar-10.1.1.tgz#f89832b9109e89bb59d516184638accd028e9735"
|
||||
integrity sha512-f9IaDJGlBzSxnJ3Ki76n2JdzfQngUFyCf0E+CuVLaR5jL0IJDcTu7vOs8wexXunRMTd8xvIv0+sdIxf8hXAGWg==
|
||||
dependencies:
|
||||
perfect-scrollbar "1.5.0"
|
||||
resize-observer-polyfill "^1.5.0"
|
||||
@@ -335,7 +366,16 @@ process-nextick-args@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
readable-stream@2.3.7, readable-stream@^2.3.7:
|
||||
readable-stream@3.6.0, readable-stream@^3.4.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@^2.3.7:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
@@ -348,15 +388,6 @@ readable-stream@2.3.7, readable-stream@^2.3.7:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.4.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
resize-observer-polyfill@^1.5.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
@@ -377,13 +408,20 @@ sax@^1.2.4:
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
semver@^7.3.4:
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
|
||||
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
|
||||
semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
shallow-clone@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
|
||||
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
|
||||
dependencies:
|
||||
kind-of "^6.0.2"
|
||||
|
||||
shell-escape@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133"
|
||||
|
@@ -57,6 +57,7 @@ export class PluginManagerService {
|
||||
}))),
|
||||
map(plugins => plugins.filter(x => x.packageName.startsWith(NAME_PREFIX))),
|
||||
map(plugins => plugins.filter(x => !BLACKLIST.includes(x.packageName))),
|
||||
map(plugins => plugins.sort((a, b) => a.name.localeCompare(b.name))),
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -188,9 +188,9 @@ object.entries@^1.1.3:
|
||||
has "^1.0.3"
|
||||
|
||||
semver@^7.1.1:
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
|
||||
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
.modal-body
|
||||
ngb-tabset([activeId]='basic')
|
||||
ngb-tab(id='basic')
|
||||
ng-template(ngbTabTitle) General
|
||||
ng-template(ngbTabContent)
|
||||
ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) General
|
||||
ng-template(ngbNavContent)
|
||||
.form-group
|
||||
label Name
|
||||
input.form-control(
|
||||
@@ -90,9 +90,9 @@
|
||||
)
|
||||
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
|
||||
|
||||
ngb-tab(id='advanced')
|
||||
ng-template(ngbTabTitle) Advanced
|
||||
ng-template(ngbTabContent)
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Advanced
|
||||
ng-template(ngbNavContent)
|
||||
.form-line
|
||||
.header
|
||||
.title Tab color
|
||||
@@ -150,9 +150,9 @@
|
||||
.title Xany
|
||||
toggle([(ngModel)]='connection.xany')
|
||||
|
||||
ngb-tab(id='scripts')
|
||||
ng-template(ngbTabTitle) Login scripts
|
||||
ng-template(ngbTabContent)
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Login scripts
|
||||
ng-template(ngbNavContent)
|
||||
table(*ngIf='connection.scripts.length > 0')
|
||||
tr
|
||||
th String to expect
|
||||
@@ -192,6 +192,8 @@
|
||||
i.fas.fa-plus
|
||||
span New item
|
||||
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
||||
|
@@ -6,7 +6,6 @@ import { first } from 'rxjs/operators'
|
||||
import { BaseTerminalTabComponent } from 'terminus-terminal'
|
||||
import { SerialService } from '../services/serial.service'
|
||||
import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
@@ -20,7 +19,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
|
||||
session: SerialSession|null = null
|
||||
serialPort: any
|
||||
private serialService: SerialService
|
||||
private homeEndSubscription: Subscription
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
@@ -33,7 +31,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
|
||||
ngOnInit () {
|
||||
this.logger = this.log.create('terminalTab')
|
||||
|
||||
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
@@ -94,18 +92,18 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
|
||||
}
|
||||
|
||||
protected attachSessionHandlers () {
|
||||
this.attachSessionHandler(this.session!.serviceMessage$.subscribe(msg => {
|
||||
this.attachSessionHandler(this.session!.serviceMessage$, msg => {
|
||||
this.write(`\r\n${colors.black.bgWhite(' Serial ')} ${msg}\r\n`)
|
||||
this.session?.resize(this.size.columns, this.size.rows)
|
||||
}))
|
||||
this.attachSessionHandler(this.session!.destroyed$.subscribe(() => {
|
||||
})
|
||||
this.attachSessionHandler(this.session!.destroyed$, () => {
|
||||
this.write('Press any key to reconnect\r\n')
|
||||
this.input$.pipe(first()).subscribe(() => {
|
||||
if (!this.session?.open) {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
}))
|
||||
})
|
||||
super.attachSessionHandlers()
|
||||
}
|
||||
|
||||
@@ -130,9 +128,4 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
|
||||
this.serialPort.update({ baudRate: rate })
|
||||
this.connection!.baudrate = rate
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.homeEndSubscription.unsubscribe()
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="16"
|
||||
viewBox="0 0 32 16"
|
||||
version="1.1"
|
||||
id="svg3749">
|
||||
<defs
|
||||
id="defs3753" />
|
||||
<g
|
||||
id="g3747"
|
||||
transform="matrix(0.48599086,0,0,0.48599086,0.50191451,-0.299629)"
|
||||
style="fill:none;fill-rule:evenodd">
|
||||
<g
|
||||
id="g3741"
|
||||
transform="translate(-292.02353,-314.25882)"
|
||||
style="fill:#ffffff">
|
||||
<path
|
||||
style="fill-rule:nonzero;stroke-width:0.10270619"
|
||||
d="M 16.007812,0 3.2929688,0.03515625 2.2324219,0.40234375 C 0.91449728,1.1071083 0,2.575555 0,3.9863281 c 0,1.2651363 1.3074352,7.8137089 1.7402344,8.7167969 0.553077,1.153703 1.988134,2.456836 3.234375,2.9375 0.8530743,0.328933 1.4753185,0.348528 10.9804686,0.357422 9.789951,0.0091 10.106534,-0.002 11.087891,-0.369141 1.221173,-0.456851 2.835858,-1.955656 3.333984,-3.09375 0.336145,-0.767943 1.638672,-7.5615083 1.638672,-8.5488279 0,-1.4107731 -0.849384,-3.02258715 -2.234375,-3.58398435 L 28.791016,0 Z m -4.27539,4.890625 c 0.427942,0 0.812664,0.071135 1.152344,0.2128906 0.342354,0.1390812 0.631097,0.3382162 0.86914,0.5976563 0.243393,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197266,0.8262256 0.197266,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201172,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146484,-0.208984 C 10.243584,10.765216 9.9516549,10.563618 9.7109375,10.298828 9.4702197,10.034039 9.2834009,9.7093672 9.1523438,9.3242188 9.0239608,8.9390707 8.9609375,8.4987146 8.9609375,8.0039062 c 0,-0.4867846 0.063023,-0.9214925 0.1914063,-1.3066406 C 9.2807263,6.3094427 9.4687769,5.9766597 9.7148438,5.7011719 9.950212,5.4390572 10.242141,5.2386906 10.589844,5.0996094 10.940222,4.9605279 11.320527,4.890625 11.732422,4.890625 Z m 9.933594,0 c 0.427942,0 0.812664,0.071135 1.152343,0.2128906 0.342354,0.1390812 0.631098,0.3382162 0.869141,0.5976563 0.243392,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197265,0.8262256 0.197265,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201171,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146485,-0.208984 -0.342353,-0.139081 -0.634282,-0.340679 -0.875,-0.605469 C 19.403813,10.034039 19.216995,9.7093672 19.085938,9.3242188 18.957555,8.9390707 18.894531,8.4987146 18.894531,8.0039062 c 0,-0.4867846 0.06302,-0.9214925 0.191407,-1.3066406 0.128382,-0.3878229 0.316433,-0.7206059 0.5625,-0.9960937 0.235368,-0.2621147 0.527296,-0.4624813 0.875,-0.6015625 0.350377,-0.1390815 0.730683,-0.2089844 1.142578,-0.2089844 z m -16.0839848,0.125 h 2.359375 V 5.625 H 7.1582031 v 4.753906 h 0.7832031 v 0.611328 H 5.5820312 V 10.378906 H 6.3652344 V 5.625 H 5.5820312 Z m 9.9335938,0 H 17.875 V 5.625 h -0.783203 v 4.753906 H 17.875 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m 9.933594,0 h 2.359375 V 5.625 h -0.783203 v 4.753906 h 0.783203 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m -13.712891,0.5625 c -0.607143,0 -1.083937,0.2102191 -1.43164,0.6328125 -0.3450291,0.4199185 -0.5175786,1.0173231 -0.5175786,1.7929687 0,0.7836694 0.1762443,1.3854905 0.5292966,1.8027344 0.353053,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529297,-1.019065 0.529297,-1.8027344 0,-0.7756456 -0.173782,-1.3730502 -0.521485,-1.7929687 C 12.812453,5.7883441 12.338122,5.578125 11.736328,5.578125 Z m 9.933594,0 c -0.607144,0 -1.083938,0.2102191 -1.431641,0.6328125 -0.345028,0.4199185 -0.517578,1.0173231 -0.517578,1.7929687 0,0.7836694 0.176244,1.3854905 0.529297,1.8027344 0.353052,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529296,-1.019065 0.529296,-1.8027344 0,-0.7756456 -0.173781,-1.3730502 -0.521484,-1.7929687 C 22.746047,5.7883441 22.271716,5.578125 21.669922,5.578125 Z"
|
||||
transform="matrix(2.0576519,0,0,2.0576519,290.99076,314.87535)"
|
||||
id="path3739" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg3749" width="32" height="16" version="1.1" viewBox="0 0 32 16"><g id="g3747" transform="matrix(0.48599086,0,0,0.48599086,0.50191451,-0.299629)" style="fill:none;fill-rule:evenodd"><g id="g3741" transform="translate(-292.02353,-314.25882)" style="fill:#fff"><path style="fill-rule:nonzero;stroke-width:.10270619" id="path3739" d="M 16.007812,0 3.2929688,0.03515625 2.2324219,0.40234375 C 0.91449728,1.1071083 0,2.575555 0,3.9863281 c 0,1.2651363 1.3074352,7.8137089 1.7402344,8.7167969 0.553077,1.153703 1.988134,2.456836 3.234375,2.9375 0.8530743,0.328933 1.4753185,0.348528 10.9804686,0.357422 9.789951,0.0091 10.106534,-0.002 11.087891,-0.369141 1.221173,-0.456851 2.835858,-1.955656 3.333984,-3.09375 0.336145,-0.767943 1.638672,-7.5615083 1.638672,-8.5488279 0,-1.4107731 -0.849384,-3.02258715 -2.234375,-3.58398435 L 28.791016,0 Z m -4.27539,4.890625 c 0.427942,0 0.812664,0.071135 1.152344,0.2128906 0.342354,0.1390812 0.631097,0.3382162 0.86914,0.5976563 0.243393,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197266,0.8262256 0.197266,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201172,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146484,-0.208984 C 10.243584,10.765216 9.9516549,10.563618 9.7109375,10.298828 9.4702197,10.034039 9.2834009,9.7093672 9.1523438,9.3242188 9.0239608,8.9390707 8.9609375,8.4987146 8.9609375,8.0039062 c 0,-0.4867846 0.063023,-0.9214925 0.1914063,-1.3066406 C 9.2807263,6.3094427 9.4687769,5.9766597 9.7148438,5.7011719 9.950212,5.4390572 10.242141,5.2386906 10.589844,5.0996094 10.940222,4.9605279 11.320527,4.890625 11.732422,4.890625 Z m 9.933594,0 c 0.427942,0 0.812664,0.071135 1.152343,0.2128906 0.342354,0.1390812 0.631098,0.3382162 0.869141,0.5976563 0.243392,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197265,0.8262256 0.197265,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201171,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146485,-0.208984 -0.342353,-0.139081 -0.634282,-0.340679 -0.875,-0.605469 C 19.403813,10.034039 19.216995,9.7093672 19.085938,9.3242188 18.957555,8.9390707 18.894531,8.4987146 18.894531,8.0039062 c 0,-0.4867846 0.06302,-0.9214925 0.191407,-1.3066406 0.128382,-0.3878229 0.316433,-0.7206059 0.5625,-0.9960937 0.235368,-0.2621147 0.527296,-0.4624813 0.875,-0.6015625 0.350377,-0.1390815 0.730683,-0.2089844 1.142578,-0.2089844 z m -16.0839848,0.125 h 2.359375 V 5.625 H 7.1582031 v 4.753906 h 0.7832031 v 0.611328 H 5.5820312 V 10.378906 H 6.3652344 V 5.625 H 5.5820312 Z m 9.9335938,0 H 17.875 V 5.625 h -0.783203 v 4.753906 H 17.875 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m 9.933594,0 h 2.359375 V 5.625 h -0.783203 v 4.753906 h 0.783203 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m -13.712891,0.5625 c -0.607143,0 -1.083937,0.2102191 -1.43164,0.6328125 -0.3450291,0.4199185 -0.5175786,1.0173231 -0.5175786,1.7929687 0,0.7836694 0.1762443,1.3854905 0.5292966,1.8027344 0.353053,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529297,-1.019065 0.529297,-1.8027344 0,-0.7756456 -0.173782,-1.3730502 -0.521485,-1.7929687 C 12.812453,5.7883441 12.338122,5.578125 11.736328,5.578125 Z m 9.933594,0 c -0.607144,0 -1.083938,0.2102191 -1.431641,0.6328125 -0.345028,0.4199185 -0.517578,1.0173231 -0.517578,1.7929687 0,0.7836694 0.176244,1.3854905 0.529297,1.8027344 0.353052,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529296,-1.019065 0.529296,-1.8027344 0,-0.7756456 -0.173781,-1.3730502 -0.521484,-1.7929687 C 22.746047,5.7883441 22.271716,5.578125 21.669922,5.578125 Z" transform="matrix(2.0576519,0,0,2.0576519,290.99076,314.87535)"/></g></g></svg>
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -6,16 +6,17 @@ import { SerialTabComponent } from './components/serialTab.component'
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
|
||||
if (recoveryToken.type === 'app:serial-tab') {
|
||||
return {
|
||||
type: SerialTabComponent,
|
||||
options: {
|
||||
connection: recoveryToken.connection,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:serial-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
|
||||
return {
|
||||
type: SerialTabComponent,
|
||||
options: {
|
||||
connection: recoveryToken.connection,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { trigger, transition, style, animate } from '@angular/animations'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { HotkeysService } from 'terminus-core'
|
||||
import { HotkeysService, BaseComponent } from 'terminus-core'
|
||||
|
||||
const INPUT_TIMEOUT = 1000
|
||||
|
||||
@@ -36,11 +35,10 @@ const INPUT_TIMEOUT = 1000
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class HotkeyInputModalComponent {
|
||||
export class HotkeyInputModalComponent extends BaseComponent {
|
||||
@Input() value: string[] = []
|
||||
@Input() timeoutProgress = 0
|
||||
|
||||
private keySubscription: Subscription
|
||||
private lastKeyEvent: number|null = null
|
||||
private keyTimeoutInterval: number|null = null
|
||||
|
||||
@@ -48,8 +46,9 @@ export class HotkeyInputModalComponent {
|
||||
private modalInstance: NgbActiveModal,
|
||||
public hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
this.hotkeys.clearCurrentKeystrokes()
|
||||
this.keySubscription = hotkeys.key.subscribe((event) => {
|
||||
this.subscribeUntilDestroyed(hotkeys.key, (event) => {
|
||||
this.lastKeyEvent = performance.now()
|
||||
this.value = this.hotkeys.getCurrentKeystrokes()
|
||||
event.preventDefault()
|
||||
@@ -75,10 +74,10 @@ export class HotkeyInputModalComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy (): void {
|
||||
this.keySubscription.unsubscribe()
|
||||
this.hotkeys.clearCurrentKeystrokes()
|
||||
this.hotkeys.enable()
|
||||
clearInterval(this.keyTimeoutInterval!)
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
close (): void {
|
||||
|
@@ -0,0 +1,23 @@
|
||||
h3.mb-3 Hotkeys
|
||||
|
||||
.input-group.mb-4
|
||||
.input-group-prepend
|
||||
.input-group-text
|
||||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
|
||||
|
||||
.form-group
|
||||
table.hotkeys-table
|
||||
tr
|
||||
th Name
|
||||
th ID
|
||||
th Hotkey
|
||||
ng-container(*ngFor='let hotkey of hotkeyDescriptions')
|
||||
tr(*ngIf='!hotkeyFilter || hotkeyFilterFn(hotkey, hotkeyFilter)')
|
||||
td {{hotkey.name}}
|
||||
td {{hotkey.id}}
|
||||
td.pr-5
|
||||
multi-hotkey-input(
|
||||
[model]='getHotkey(hotkey.id) || []',
|
||||
(modelChange)='setHotkey(hotkey.id, $event)'
|
||||
)
|
@@ -0,0 +1,7 @@
|
||||
.hotkeys-table {
|
||||
margin-top: 30px;
|
||||
|
||||
td, th {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, NgZone } from '@angular/core'
|
||||
import {
|
||||
ConfigService,
|
||||
HotkeyDescription,
|
||||
HotkeysService,
|
||||
HostAppService,
|
||||
} from 'terminus-core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'hotkey-settings-tab',
|
||||
template: require('./hotkeySettingsTab.component.pug'),
|
||||
styles: [
|
||||
require('./hotkeySettingsTab.component.scss'),
|
||||
],
|
||||
})
|
||||
export class HotkeySettingsTabComponent {
|
||||
hotkeyFilter = ''
|
||||
hotkeyDescriptions: HotkeyDescription[]
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
public hostApp: HostAppService,
|
||||
public zone: NgZone,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
hotkeys.getHotkeyDescriptions().then(descriptions => {
|
||||
this.hotkeyDescriptions = descriptions
|
||||
})
|
||||
}
|
||||
|
||||
getHotkey (id: string) {
|
||||
let ptr = this.config.store.hotkeys
|
||||
for (const token of id.split(/\./g)) {
|
||||
ptr = ptr[token]
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
setHotkey (id: string, value) {
|
||||
let ptr = this.config.store
|
||||
let prop = 'hotkeys'
|
||||
for (const token of id.split(/\./g)) {
|
||||
ptr = ptr[prop]
|
||||
prop = token
|
||||
}
|
||||
ptr[prop] = value
|
||||
this.config.save()
|
||||
}
|
||||
|
||||
hotkeyFilterFn (hotkey: HotkeyDescription, query: string): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
const s = hotkey.name + (this.getHotkey(hotkey.id) || []).toString()
|
||||
return s.toLowerCase().includes(query.toLowerCase())
|
||||
}
|
||||
}
|
@@ -1,385 +1,108 @@
|
||||
button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
|
||||
|
||||
ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
||||
ngb-tab(id='application')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-window-maximize.mr-2
|
||||
| Application
|
||||
ng-template(ngbTabContent)
|
||||
.d-flex.align-items-center.flex-wrap.mb-4
|
||||
h1.terminus-title.mb-2.mr-2 Terminus
|
||||
.content
|
||||
ul.nav-pills(ngbNav, #nav='ngbNav', [activeId]='activeTab', orientation='vertical')
|
||||
li(ngbNavItem='application')
|
||||
a(ngbNavLink)
|
||||
i.fas.fa-fw.fa-window-maximize.mr-2
|
||||
| Application
|
||||
ng-template(ngbNavContent)
|
||||
.terminus-logo.mt-3
|
||||
h1.terminus-title Terminus
|
||||
sup α
|
||||
|
||||
.text-muted.mr-auto {{homeBase.appVersion}}
|
||||
.text-center
|
||||
.text-muted {{homeBase.appVersion}}
|
||||
|
||||
div
|
||||
button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()')
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
.mb-5.mt-3
|
||||
button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()')
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
|
||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
||||
button.btn.btn-secondary.mr-3((click)='homeBase.reportBug()')
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
|
||||
button.btn.btn-secondary(
|
||||
*ngIf='!updateAvailable',
|
||||
(click)='checkForUpdates()',
|
||||
[disabled]='checkingForUpdate'
|
||||
)
|
||||
i.fas.fa-sync(
|
||||
[class.fa-spin]='checkingForUpdate'
|
||||
)
|
||||
span Check for updates
|
||||
|
||||
button.btn.btn-info(
|
||||
*ngIf='updateAvailable',
|
||||
(click)='updater.update()',
|
||||
)
|
||||
i.fas.fa-sync
|
||||
span Update
|
||||
|
||||
.form-line(*ngIf='hostApp.platform !== Platform.Linux')
|
||||
.header
|
||||
.title Shell integration
|
||||
.description Allows quickly opening a terminal in the selected folder
|
||||
toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable analytics
|
||||
.description We're only tracking your Terminus and OS versions.
|
||||
toggle(
|
||||
[(ngModel)]='config.store.enableAnalytics',
|
||||
(ngModelChange)='saveConfiguration(true)',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Automatic Updates
|
||||
.description Enable automatic installation of updates when they become available.
|
||||
toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Debugging
|
||||
|
||||
button.btn.btn-secondary((click)='hostApp.openDevTools()')
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
span Open DevTools
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Theme
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.appearance.theme',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
)
|
||||
option(*ngFor='let theme of themes', [ngValue]='theme.name') {{theme.name}}
|
||||
li(*ngFor='let provider of settingsProviders', [ngbNavItem]='provider.id')
|
||||
a(ngbNavLink)
|
||||
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
|
||||
| {{provider.title}}
|
||||
ng-template(ngbNavContent)
|
||||
settings-tab-body([provider]='provider')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Tabs location
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.tabsLocation',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"top"'
|
||||
)
|
||||
| Top
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"bottom"'
|
||||
)
|
||||
| Bottom
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"left"'
|
||||
)
|
||||
| Left
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"right"'
|
||||
)
|
||||
| Right
|
||||
li(ngbNavItem='config-file')
|
||||
a(ngbNavLink)
|
||||
i.fas.fa-fw.fa-code.mr-2
|
||||
| Config file
|
||||
ng-template.test(ngbNavContent)
|
||||
.d-flex.flex-column.w-100.h-100
|
||||
.h-100.d-flex
|
||||
.w-100.d-flex.flex-column
|
||||
h3 Config File
|
||||
textarea.form-control.h-100(
|
||||
[(ngModel)]='configFile'
|
||||
)
|
||||
.w-100.d-flex.flex-column
|
||||
h3 Defaults
|
||||
textarea.form-control.h-100(
|
||||
[(ngModel)]='configDefaults',
|
||||
readonly
|
||||
)
|
||||
.mt-3.d-flex
|
||||
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
|
||||
i.fas.fa-check.mr-2
|
||||
| Save and apply
|
||||
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
|
||||
i.fas.fa-exclamation-triangle.mr-2
|
||||
| Invalid syntax
|
||||
button.btn.btn-secondary.ml-auto((click)='showConfigFile()')
|
||||
i.fas.fa-external-link-square-alt.mr-2
|
||||
| Show config file
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Tabs width
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.flexTabs',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| Dynamic
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Fixed
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(*ngIf='hostApp.platform !== Platform.macOS') Acrylic background
|
||||
.title(*ngIf='hostApp.platform === Platform.macOS') Vibrancy
|
||||
.description Gives the window a blurred transparent background
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.vibrancy',
|
||||
(ngModelChange)='saveConfiguration()'
|
||||
)
|
||||
|
||||
.form-line(*ngIf='config.store.appearance.vibrancy && isFluentVibrancySupported')
|
||||
.header
|
||||
.title Background type
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.vibrancyType',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"blur"'
|
||||
)
|
||||
| Blur
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"fluent"'
|
||||
)
|
||||
| Fluent
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Opacity
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.opacity',
|
||||
(ngModelChange)='saveConfiguration(); (hostApp.platform === Platform.Linux && config.requestRestart())',
|
||||
min='0.4',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.form-line(*ngIf='hostApp.platform !== Platform.Linux')
|
||||
.header
|
||||
.title Shell integration
|
||||
.description Allows quickly opening a terminal in the selected folder
|
||||
toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Window frame
|
||||
.description Whether a custom window or an OS native window should be used
|
||||
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.frame',
|
||||
(ngModelChange)='saveConfiguration(true)',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"native"'
|
||||
)
|
||||
| Native
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"thin"'
|
||||
)
|
||||
| Thin
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"full"'
|
||||
)
|
||||
| Full
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Dock the terminal
|
||||
.description Snaps the window to a side of the screen
|
||||
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.dock',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"off"'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"top"'
|
||||
)
|
||||
| Top
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"left"'
|
||||
)
|
||||
| Left
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"right"'
|
||||
)
|
||||
| Right
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"bottom"'
|
||||
)
|
||||
| Bottom
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Display on
|
||||
.description Snaps the window to a side of the screen
|
||||
|
||||
div(
|
||||
[(ngModel)]='config.store.appearance.dockScreen',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
value='current'
|
||||
)
|
||||
| Current
|
||||
label.btn.btn-secondary(*ngFor='let screen of screens', ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='screen.id'
|
||||
)
|
||||
| {{screen.name}}
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Dock always on top
|
||||
.description Keep docked terminal always on top
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.dockAlwaysOnTop',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Docked terminal size
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.dockFill',
|
||||
(mouseup)='saveConfiguration(); docking.dock()',
|
||||
min='0.05',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Docked terminal space
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.dockSpace',
|
||||
(mouseup)='saveConfiguration(); docking.dock()',
|
||||
min='0.2',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Hide dock on blur
|
||||
.description Hides the docked terminal when you click away.
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.dockHideOnBlur',
|
||||
(ngModelChange)='saveConfiguration(); ',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Debugging
|
||||
|
||||
button.btn.btn-secondary((click)='hostApp.openDevTools()')
|
||||
i.fas.fa-bug
|
||||
span Open DevTools
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable analytics
|
||||
.description We're only tracking your Terminus and OS versions.
|
||||
toggle(
|
||||
[(ngModel)]='config.store.enableAnalytics',
|
||||
(ngModelChange)='saveConfiguration(true)',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Automatic Updates
|
||||
.description Enable automatic installation of updates when they become available.
|
||||
toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Custom CSS
|
||||
textarea.form-control(
|
||||
[(ngModel)]='config.store.appearance.css',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
)
|
||||
|
||||
ngb-tab(id='hotkeys')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-keyboard.mr-2
|
||||
| Hotkeys
|
||||
ng-template(ngbTabContent)
|
||||
h3.mb-3 Hotkeys
|
||||
|
||||
.input-group.mb-4
|
||||
.input-group-prepend
|
||||
.input-group-text
|
||||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
|
||||
|
||||
.form-group
|
||||
table.hotkeys-table
|
||||
tr
|
||||
th Name
|
||||
th ID
|
||||
th Hotkey
|
||||
ng-container(*ngFor='let hotkey of hotkeyDescriptions')
|
||||
tr(*ngIf='!hotkeyFilter || hotkeyFilterFn(hotkey, hotkeyFilter)')
|
||||
td {{hotkey.name}}
|
||||
td {{hotkey.id}}
|
||||
td.pr-5
|
||||
multi-hotkey-input(
|
||||
[model]='getHotkey(hotkey.id) || []',
|
||||
(modelChange)='setHotkey(hotkey.id, $event); saveConfiguration(); docking.dock()'
|
||||
)
|
||||
|
||||
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
|
||||
ng-template(ngbTabTitle)
|
||||
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
|
||||
| {{provider.title}}
|
||||
ng-template(ngbTabContent)
|
||||
settings-tab-body([provider]='provider')
|
||||
|
||||
|
||||
ngb-tab(id='config-file')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-code.mr-2
|
||||
| Config file
|
||||
ng-template.test(ngbTabContent)
|
||||
.d-flex.flex-column.w-100.h-100
|
||||
.h-100.d-flex
|
||||
.w-100.d-flex.flex-column
|
||||
h3 Config File
|
||||
textarea.form-control.h-100(
|
||||
[(ngModel)]='configFile'
|
||||
)
|
||||
.w-100.d-flex.flex-column
|
||||
h3 Defaults
|
||||
textarea.form-control.h-100(
|
||||
[(ngModel)]='configDefaults',
|
||||
readonly
|
||||
)
|
||||
.mt-2.mb-2.d-flex
|
||||
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
|
||||
i.fas.fa-check.mr-2
|
||||
| Save and apply
|
||||
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
|
||||
i.fas.fa-exclamation-triangle.mr-2
|
||||
| Invalid syntax
|
||||
button.btn.btn-secondary.ml-auto((click)='showConfigFile()')
|
||||
i.fas.fa-external-link-square-alt.mr-2
|
||||
| Show config file
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
@@ -1,20 +1,39 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
flex-direction: column;
|
||||
|
||||
>.btn-block {
|
||||
margin: 20px;
|
||||
width: auto;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hotkeys-table {
|
||||
margin-top: 30px;
|
||||
> .content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 0 0;
|
||||
|
||||
td, th {
|
||||
padding: 5px 10px;
|
||||
> .nav {
|
||||
padding: 20px 10px;
|
||||
width: 190px;
|
||||
flex: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
> .tab-content {
|
||||
flex: auto;
|
||||
padding: 20px 30px;
|
||||
overflow-y: auto;
|
||||
|
||||
> ::ng-deep .tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pad-window-controls > .content > .nav {
|
||||
padding-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,22 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import * as yaml from 'js-yaml'
|
||||
import { debounce } from 'utils-decorators/dist/cjs'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Component, Inject, Input, HostBinding, NgZone } from '@angular/core'
|
||||
import {
|
||||
ElectronService,
|
||||
DockingService,
|
||||
ConfigService,
|
||||
HotkeyDescription,
|
||||
HotkeysService,
|
||||
BaseTabComponent,
|
||||
Theme,
|
||||
HostAppService,
|
||||
Platform,
|
||||
HomeBaseService,
|
||||
ShellIntegrationService,
|
||||
isWindowsBuild,
|
||||
WIN_BUILD_FLUENT_BG_SUPPORTED,
|
||||
UpdaterService,
|
||||
} from 'terminus-core'
|
||||
|
||||
import { SettingsTabProvider } from '../api'
|
||||
@@ -27,39 +21,32 @@ import { SettingsTabProvider } from '../api'
|
||||
template: require('./settingsTab.component.pug'),
|
||||
styles: [
|
||||
require('./settingsTab.component.scss'),
|
||||
require('./settingsTab.deep.component.css'),
|
||||
],
|
||||
})
|
||||
export class SettingsTabComponent extends BaseTabComponent {
|
||||
@Input() activeTab: string
|
||||
hotkeyFilter = ''
|
||||
hotkeyDescriptions: HotkeyDescription[]
|
||||
screens: any[]
|
||||
Platform = Platform
|
||||
configDefaults: any
|
||||
configFile: string
|
||||
isShellIntegrationInstalled = false
|
||||
isFluentVibrancySupported = false
|
||||
checkingForUpdate = false
|
||||
updateAvailable = false
|
||||
@HostBinding('class.pad-window-controls') padWindowControls = false
|
||||
private configSubscription: Subscription
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private electron: ElectronService,
|
||||
public docking: DockingService,
|
||||
public hostApp: HostAppService,
|
||||
public homeBase: HomeBaseService,
|
||||
public shellIntegration: ShellIntegrationService,
|
||||
public zone: NgZone,
|
||||
hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
@Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[],
|
||||
@Inject(Theme) public themes: Theme[],
|
||||
) {
|
||||
super()
|
||||
this.setTitle('Settings')
|
||||
this.screens = this.docking.getScreens()
|
||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||
this.themes = config.enabledServices(this.themes)
|
||||
this.settingsProviders.sort((a, b) => a.title.localeCompare(b.title))
|
||||
|
||||
this.configDefaults = yaml.dump(config.getDefaults())
|
||||
|
||||
@@ -69,18 +56,8 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
&& config.store.appearance.tabsLocation !== 'top'
|
||||
}
|
||||
|
||||
this.configSubscription = config.changed$.subscribe(onConfigChange)
|
||||
this.subscribeUntilDestroyed(config.changed$, onConfigChange)
|
||||
onConfigChange()
|
||||
|
||||
hostApp.displaysChanged$.subscribe(() => {
|
||||
this.zone.run(() => this.screens = this.docking.getScreens())
|
||||
})
|
||||
|
||||
hotkeys.getHotkeyDescriptions().then(descriptions => {
|
||||
this.hotkeyDescriptions = descriptions
|
||||
})
|
||||
|
||||
this.isFluentVibrancySupported = isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -97,7 +74,6 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.configSubscription.unsubscribe()
|
||||
this.config.save()
|
||||
}
|
||||
|
||||
@@ -132,27 +108,9 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
getHotkey (id: string) {
|
||||
let ptr = this.config.store.hotkeys
|
||||
for (const token of id.split(/\./g)) {
|
||||
ptr = ptr[token]
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
setHotkey (id: string, value) {
|
||||
let ptr = this.config.store
|
||||
let prop = 'hotkeys'
|
||||
for (const token of id.split(/\./g)) {
|
||||
ptr = ptr[prop]
|
||||
prop = token
|
||||
}
|
||||
ptr[prop] = value
|
||||
}
|
||||
|
||||
hotkeyFilterFn (hotkey: HotkeyDescription, query: string): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
const s = hotkey.name + (this.getHotkey(hotkey.id) || []).toString()
|
||||
return s.toLowerCase().includes(query.toLowerCase())
|
||||
async checkForUpdates () {
|
||||
this.checkingForUpdate = true
|
||||
this.updateAvailable = await this.updater.check()
|
||||
this.checkingForUpdate = false
|
||||
}
|
||||
}
|
||||
|
@@ -1,28 +0,0 @@
|
||||
:host /deep/ ngb-tabset {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host /deep/ ngb-tabset > .nav {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:host /deep/ ngb-tabset > .tab-content {
|
||||
padding: 15px 30px;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host /deep/ ngb-tabset > .tab-content > .tab-pane {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host.pad-window-controls /deep/ ngb-tabset > .nav {
|
||||
padding-top: 40px;
|
||||
}
|
279
terminus-settings/src/components/windowSettingsTab.component.pug
Normal file
@@ -0,0 +1,279 @@
|
||||
h3.mb-3 Window
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Theme
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.appearance.theme',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
)
|
||||
option(*ngFor='let theme of themes', [ngValue]='theme.name') {{theme.name}}
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(*ngIf='hostApp.platform !== Platform.macOS') Acrylic background
|
||||
.title(*ngIf='hostApp.platform === Platform.macOS') Vibrancy
|
||||
.description Gives the window a blurred transparent background
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.vibrancy',
|
||||
(ngModelChange)='saveConfiguration()'
|
||||
)
|
||||
|
||||
.form-line(*ngIf='config.store.appearance.vibrancy && isFluentVibrancySupported')
|
||||
.header
|
||||
.title Background type
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.vibrancyType',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"blur"'
|
||||
)
|
||||
| Blur
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"fluent"'
|
||||
)
|
||||
| Fluent
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Opacity
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.opacity',
|
||||
(ngModelChange)='saveConfiguration(); (hostApp.platform === Platform.Linux && config.requestRestart())',
|
||||
min='0.4',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Window frame
|
||||
.description Whether a custom window or an OS native window should be used
|
||||
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.frame',
|
||||
(ngModelChange)='saveConfiguration(true)',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"native"'
|
||||
)
|
||||
| Native
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"thin"'
|
||||
)
|
||||
| Thin
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"full"'
|
||||
)
|
||||
| Full
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Dock the terminal
|
||||
.description Snaps the window to a side of the screen
|
||||
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.dock',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"off"'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"top"'
|
||||
)
|
||||
| Top
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"left"'
|
||||
)
|
||||
| Left
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"right"'
|
||||
)
|
||||
| Right
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"bottom"'
|
||||
)
|
||||
| Bottom
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Display on
|
||||
.description Snaps the window to a side of the screen
|
||||
|
||||
div(
|
||||
[(ngModel)]='config.store.appearance.dockScreen',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
value='current'
|
||||
)
|
||||
| Current
|
||||
label.btn.btn-secondary(*ngFor='let screen of screens', ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='screen.id'
|
||||
)
|
||||
| {{screen.name}}
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Dock always on top
|
||||
.description Keep docked terminal always on top
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.dockAlwaysOnTop',
|
||||
(ngModelChange)='saveConfiguration(); docking.dock()',
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Docked terminal size
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.dockFill',
|
||||
(mouseup)='saveConfiguration(); docking.dock()',
|
||||
min='0.05',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Docked terminal space
|
||||
input(
|
||||
type='range',
|
||||
[(ngModel)]='config.store.appearance.dockSpace',
|
||||
(mouseup)='saveConfiguration(); docking.dock()',
|
||||
min='0.2',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
|
||||
.header
|
||||
.title Hide dock on blur
|
||||
.description Hides the docked terminal when you click away.
|
||||
toggle(
|
||||
[(ngModel)]='config.store.appearance.dockHideOnBlur',
|
||||
(ngModelChange)='saveConfiguration(); ',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Tabs location
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.tabsLocation',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"top"'
|
||||
)
|
||||
| Top
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"bottom"'
|
||||
)
|
||||
| Bottom
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"left"'
|
||||
)
|
||||
| Left
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='"right"'
|
||||
)
|
||||
| Right
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Tabs width
|
||||
.btn-group(
|
||||
[(ngModel)]='config.store.appearance.flexTabs',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| Dynamic
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Fixed
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Hide tab index
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.terminal.hideTabIndex',
|
||||
(ngModelChange)='config.save();',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Hide tab close button
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.terminal.hideCloseButton',
|
||||
(ngModelChange)='config.save();',
|
||||
)
|
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { debounce } from 'utils-decorators/dist/cjs'
|
||||
import { Component, Inject, NgZone } from '@angular/core'
|
||||
import {
|
||||
DockingService,
|
||||
ConfigService,
|
||||
Theme,
|
||||
HostAppService,
|
||||
Platform,
|
||||
isWindowsBuild,
|
||||
WIN_BUILD_FLUENT_BG_SUPPORTED,
|
||||
BaseComponent,
|
||||
} from 'terminus-core'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'window-settings-tab',
|
||||
template: require('./windowSettingsTab.component.pug'),
|
||||
})
|
||||
export class WindowSettingsTabComponent extends BaseComponent {
|
||||
screens: any[]
|
||||
Platform = Platform
|
||||
isFluentVibrancySupported = false
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
public docking: DockingService,
|
||||
public hostApp: HostAppService,
|
||||
public zone: NgZone,
|
||||
@Inject(Theme) public themes: Theme[],
|
||||
) {
|
||||
super()
|
||||
this.screens = this.docking.getScreens()
|
||||
this.themes = config.enabledServices(this.themes)
|
||||
|
||||
this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
|
||||
this.zone.run(() => this.screens = this.docking.getScreens())
|
||||
})
|
||||
|
||||
this.isFluentVibrancySupported = isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
saveConfiguration (requireRestart?: boolean) {
|
||||
this.config.save()
|
||||
if (requireRestart) {
|
||||
this.config.requestRestart()
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,13 +6,17 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import TerminusCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
|
||||
|
||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
import { SettingsTabBodyComponent } from './components/settingsTabBody.component'
|
||||
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
||||
|
||||
import { SettingsTabProvider } from './api'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SettingsHotkeyProvider } from './hotkeys'
|
||||
import { SettingsConfigProvider } from './config'
|
||||
import { HotkeySettingsTabProvider, WindowSettingsTabProvider } from './settings'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
@@ -26,16 +30,22 @@ import { SettingsConfigProvider } from './config'
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: SettingsConfigProvider, multi: true },
|
||||
{ provide: HotkeyProvider, useClass: SettingsHotkeyProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: HotkeySettingsTabProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: WindowSettingsTabProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
SettingsTabComponent,
|
||||
WindowSettingsTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
MultiHotkeyInputComponent,
|
||||
SettingsTabComponent,
|
||||
SettingsTabBodyComponent,
|
||||
WindowSettingsTabComponent,
|
||||
],
|
||||
})
|
||||
export default class SettingsModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
29
terminus-settings/src/settings.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider } from './api'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { WindowSettingsTabComponent } from './components/windowSettingsTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class HotkeySettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'hotkeys'
|
||||
icon = 'keyboard'
|
||||
title = 'Hotkeys'
|
||||
|
||||
getComponentType (): any {
|
||||
return HotkeySettingsTabComponent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class WindowSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'window'
|
||||
icon = 'window-maximize'
|
||||
title = 'Window'
|
||||
|
||||
getComponentType (): any {
|
||||
return WindowSettingsTabComponent
|
||||
}
|
||||
}
|
@@ -25,10 +25,11 @@
|
||||
"@types/ssh2": "^0.5.35",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"clone-deep": "^4.0.1",
|
||||
"ssh2": "^0.8.9",
|
||||
"ssh2-streams": "Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5",
|
||||
"sshpk": "^1.16.1",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"strip-ansi": "^7.0.0",
|
||||
"temp": "^0.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@@ -6,6 +6,7 @@ import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { Client, ClientChannel } from 'ssh2'
|
||||
import { Logger } from 'terminus-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { ProxyCommandStream } from './services/ssh.service'
|
||||
|
||||
export interface LoginScript {
|
||||
expect: string
|
||||
@@ -42,6 +43,8 @@ export interface SSHConnection {
|
||||
agentForward?: boolean
|
||||
warnOnClose?: boolean
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
forwardedPorts?: ForwardedPortConfig[]
|
||||
}
|
||||
|
||||
export enum PortForwardType {
|
||||
@@ -50,7 +53,15 @@ export enum PortForwardType {
|
||||
Dynamic = 'Dynamic',
|
||||
}
|
||||
|
||||
export class ForwardedPort {
|
||||
export interface ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host: string
|
||||
port: number
|
||||
targetAddress: string
|
||||
targetPort: number
|
||||
}
|
||||
|
||||
export class ForwardedPort implements ForwardedPortConfig {
|
||||
type: PortForwardType
|
||||
host = '127.0.0.1'
|
||||
port: number
|
||||
@@ -117,6 +128,7 @@ export class SSHSession extends BaseSession {
|
||||
forwardedPorts: ForwardedPort[] = []
|
||||
logger: Logger
|
||||
jumpStream: any
|
||||
proxyCommandStream: ProxyCommandStream|null = null
|
||||
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
private serviceMessage = new Subject<string>()
|
||||
@@ -136,6 +148,11 @@ export class SSHSession extends BaseSession {
|
||||
async start (): Promise<void> {
|
||||
this.open = true
|
||||
|
||||
this.proxyCommandStream?.on('error', err => {
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||
this.destroy()
|
||||
})
|
||||
|
||||
try {
|
||||
this.shell = await this.openShellChannel({ x11: this.connection.x11 })
|
||||
} catch (err) {
|
||||
@@ -361,6 +378,7 @@ export class SSHSession extends BaseSession {
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
this.serviceMessage.complete()
|
||||
this.proxyCommandStream?.destroy()
|
||||
await super.destroy()
|
||||
}
|
||||
|
||||
@@ -407,11 +425,6 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
}
|
||||
|
||||
export interface SSHConnectionGroup {
|
||||
name: string
|
||||
connections: SSHConnection[]
|
||||
}
|
||||
|
||||
export const ALGORITHM_BLACKLIST = [
|
||||
// cause native crashes in node crypto, use EC instead
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
|
@@ -1,8 +1,8 @@
|
||||
.modal-body
|
||||
ngb-tabset([activeId]='basic')
|
||||
ngb-tab(id='basic')
|
||||
ng-template(ngbTabTitle) General
|
||||
ng-template(ngbTabContent)
|
||||
ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) General
|
||||
ng-template(ngbNavContent)
|
||||
.form-group
|
||||
label Name
|
||||
input.form-control(
|
||||
@@ -17,10 +17,11 @@
|
||||
type='text',
|
||||
placeholder='Ungrouped',
|
||||
[(ngModel)]='connection.group',
|
||||
[ngbTypeahead]='groupTypeahead',
|
||||
)
|
||||
|
||||
.d-flex
|
||||
.form-group
|
||||
.d-flex.w-100(*ngIf='!useProxyCommand')
|
||||
.form-group.w-100.mr-4
|
||||
label Host
|
||||
input.form-control(
|
||||
type='text',
|
||||
@@ -35,6 +36,9 @@
|
||||
[(ngModel)]='connection.port',
|
||||
)
|
||||
|
||||
.alert.alert-info(*ngIf='useProxyCommand')
|
||||
.mr-auto Using a proxy command instead of a network connection
|
||||
|
||||
.form-group
|
||||
label Username
|
||||
input.form-control(
|
||||
@@ -42,30 +46,30 @@
|
||||
[(ngModel)]='connection.user',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Authentication
|
||||
.form-group
|
||||
label Authentication method
|
||||
|
||||
.btn-group.w-100(
|
||||
[(ngModel)]='connection.auth',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-outline-secondary(ngbButtonLabel)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(type='radio', ngbButton, [value]='null')
|
||||
i.far.fa-lightbulb
|
||||
.m-0 Auto
|
||||
label.btn.btn-outline-secondary(ngbButtonLabel)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(type='radio', ngbButton, [value]='"password"')
|
||||
i.fas.fa-font
|
||||
.m-0 Password
|
||||
label.btn.btn-outline-secondary(ngbButtonLabel)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(type='radio', ngbButton, [value]='"publicKey"')
|
||||
i.fas.fa-key
|
||||
.m-0 Key
|
||||
label.btn.btn-outline-secondary(ngbButtonLabel)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(type='radio', ngbButton, [value]='"agent"')
|
||||
i.fas.fa-user-secret
|
||||
.m-0 Agent
|
||||
label.btn.btn-outline-secondary(ngbButtonLabel)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(type='radio', ngbButton, [value]='"keyboardInteractive"')
|
||||
i.far.fa-keyboard
|
||||
.m-0 Interactive
|
||||
@@ -96,10 +100,19 @@
|
||||
button.btn.btn-secondary((click)='selectPrivateKey()')
|
||||
i.fas.fa-folder-open
|
||||
|
||||
ngb-tab(id='advanced')
|
||||
ng-template(ngbTabTitle) Advanced
|
||||
ng-template(ngbTabContent)
|
||||
.form-line
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Ports
|
||||
ng-template(ngbNavContent)
|
||||
ssh-port-forwarding-config(
|
||||
[model]='connection.forwardedPorts',
|
||||
(forwardAdded)='onForwardAdded($event)',
|
||||
(forwardRemoved)='onForwardRemoved($event)'
|
||||
)
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Advanced
|
||||
ng-template(ngbNavContent)
|
||||
.form-line(*ngIf='!useProxyCommand')
|
||||
.header
|
||||
.title Jump host
|
||||
select.form-control([(ngModel)]='connection.jumpHost')
|
||||
@@ -113,7 +126,7 @@
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Agent Forwarding
|
||||
.title Agent forwarding
|
||||
toggle([(ngModel)]='connection.agentForward')
|
||||
|
||||
.form-line
|
||||
@@ -165,9 +178,22 @@
|
||||
[(ngModel)]='connection.readyTimeout',
|
||||
)
|
||||
|
||||
ngb-tab(id='ciphers')
|
||||
ng-template(ngbTabTitle) Ciphers
|
||||
ng-template(ngbTabContent)
|
||||
.form-line(*ngIf='!connection.jumpHost')
|
||||
.header
|
||||
.title Use a proxy command
|
||||
.description Command's stdin/stdout is used instead of a network connection
|
||||
toggle([(ngModel)]='useProxyCommand')
|
||||
|
||||
.form-group(*ngIf='useProxyCommand && !connection.jumpHost')
|
||||
label Proxy command
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.proxyCommand',
|
||||
)
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Ciphers
|
||||
ng-template(ngbNavContent)
|
||||
.form-line.align-items-start
|
||||
.header
|
||||
.title Ciphers
|
||||
@@ -196,10 +222,9 @@
|
||||
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
|
||||
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
|
||||
|
||||
|
||||
ngb-tab(id='scripts')
|
||||
ng-template(ngbTabTitle) Login scripts
|
||||
ng-template(ngbTabContent)
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Login scripts
|
||||
ng-template(ngbNavContent)
|
||||
table(*ngIf='connection.scripts.length > 0')
|
||||
tr
|
||||
th String to expect
|
||||
@@ -239,6 +264,8 @@
|
||||
i.fas.fa-plus
|
||||
span New item
|
||||
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
||||
|
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
|
||||
|
||||
import { ElectronService, HostAppService, ConfigService } from 'terminus-core'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { SSHConnection, LoginScript, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
|
||||
import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
|
||||
import { PromptModalComponent } from './promptModal.component'
|
||||
import { ALGORITHMS } from 'ssh2-streams/lib/constants'
|
||||
|
||||
@@ -14,11 +17,14 @@ import { ALGORITHMS } from 'ssh2-streams/lib/constants'
|
||||
export class EditConnectionModalComponent {
|
||||
connection: SSHConnection
|
||||
hasSavedPassword: boolean
|
||||
useProxyCommand: boolean
|
||||
|
||||
supportedAlgorithms: Record<string, string> = {}
|
||||
defaultAlgorithms: Record<string, string[]> = {}
|
||||
algorithms: Record<string, Record<string, boolean>> = {}
|
||||
|
||||
private groupNames: string[]
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private modalInstance: NgbActiveModal,
|
||||
@@ -43,14 +49,26 @@ export class EditConnectionModalComponent {
|
||||
this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
||||
this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
||||
}
|
||||
|
||||
this.groupNames = [...new Set(config.store.ssh.connections.map(x => x.group))] as string[]
|
||||
this.groupNames = this.groupNames.filter(x => x).sort()
|
||||
}
|
||||
|
||||
groupTypeahead = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase())))
|
||||
)
|
||||
|
||||
async ngOnInit () {
|
||||
this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.connection)
|
||||
this.connection.algorithms = this.connection.algorithms ?? {}
|
||||
this.connection.scripts = this.connection.scripts ?? []
|
||||
this.connection.auth = this.connection.auth ?? null
|
||||
|
||||
this.useProxyCommand = !!this.connection.proxyCommand
|
||||
|
||||
for (const k of Object.values(SSHAlgorithmType)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!this.connection.algorithms[k]) {
|
||||
@@ -102,6 +120,9 @@ export class EditConnectionModalComponent {
|
||||
.filter(([_, v]) => !!v)
|
||||
.map(([key, _]) => key)
|
||||
}
|
||||
if (!this.useProxyCommand) {
|
||||
this.connection.proxyCommand = undefined
|
||||
}
|
||||
this.modalInstance.close(this.connection)
|
||||
}
|
||||
|
||||
@@ -152,4 +173,13 @@ export class EditConnectionModalComponent {
|
||||
}
|
||||
this.connection.scripts.push({ expect: '', send: '' })
|
||||
}
|
||||
|
||||
onForwardAdded (fw: ForwardedPortConfig) {
|
||||
this.connection.forwardedPorts = this.connection.forwardedPorts ?? []
|
||||
this.connection.forwardedPorts.push(fw)
|
||||
}
|
||||
|
||||
onForwardRemoved (fw: ForwardedPortConfig) {
|
||||
this.connection.forwardedPorts = this.connection.forwardedPorts?.filter(x => x !== fw)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
.list-group-light.mb-3
|
||||
.list-group-item.d-flex.align-items-center(*ngFor='let fw of model')
|
||||
strong(*ngIf='fw.type === PortForwardType.Local') Local
|
||||
strong(*ngIf='fw.type === PortForwardType.Remote') Remote
|
||||
strong(*ngIf='fw.type === PortForwardType.Dynamic') Dynamic
|
||||
.ml-3 {{fw.host}}:{{fw.port}}
|
||||
.ml-2 →
|
||||
.ml-2(*ngIf='fw.type !== PortForwardType.Dynamic') {{fw.targetAddress}}:{{fw.targetPort}}
|
||||
.ml-2(*ngIf='fw.type === PortForwardType.Dynamic') SOCKS proxy
|
||||
button.btn.btn-link.ml-auto((click)='remove(fw)')
|
||||
i.fas.fa-trash-alt.mr-2
|
||||
span Remove
|
||||
|
||||
.input-group.mb-2(*ngIf='newForward.type === PortForwardType.Dynamic')
|
||||
input.form-control(type='text', [(ngModel)]='newForward.host')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.port')
|
||||
|
||||
.input-group.mb-2(*ngIf='newForward.type !== PortForwardType.Dynamic')
|
||||
input.form-control(type='text', [(ngModel)]='newForward.host')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.port')
|
||||
.input-group-append
|
||||
.input-group-text →
|
||||
input.form-control(type='text', [(ngModel)]='newForward.targetAddress')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.targetPort')
|
||||
|
||||
.d-flex
|
||||
.btn-group.mr-auto(
|
||||
[(ngModel)]='newForward.type',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Local'
|
||||
)
|
||||
| Local
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Remote'
|
||||
)
|
||||
| Remote
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Dynamic'
|
||||
)
|
||||
| Dynamic
|
||||
|
||||
button.btn.btn-primary((click)='addForward()')
|
||||
i.fas.fa-check.mr-2
|
||||
span Forward port
|
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
import { ForwardedPortConfig, PortForwardType } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'ssh-port-forwarding-config',
|
||||
template: require('./sshPortForwardingConfig.component.pug'),
|
||||
})
|
||||
export class SSHPortForwardingConfigComponent {
|
||||
@Input() model: ForwardedPortConfig[]
|
||||
@Output() forwardAdded = new EventEmitter<ForwardedPortConfig>()
|
||||
@Output() forwardRemoved = new EventEmitter<ForwardedPortConfig>()
|
||||
newForward: ForwardedPortConfig
|
||||
PortForwardType = PortForwardType
|
||||
|
||||
constructor (
|
||||
) {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.newForward = {
|
||||
type: PortForwardType.Local,
|
||||
host: '127.0.0.1',
|
||||
port: 8000,
|
||||
targetAddress: '127.0.0.1',
|
||||
targetPort: 80,
|
||||
}
|
||||
}
|
||||
|
||||
async addForward () {
|
||||
try {
|
||||
this.forwardAdded.emit(this.newForward)
|
||||
this.reset()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
remove (fw: ForwardedPortConfig) {
|
||||
this.forwardRemoved.emit(fw)
|
||||
}
|
||||
}
|
@@ -2,64 +2,8 @@
|
||||
h5.m-0 Port forwarding
|
||||
|
||||
.modal-body.pt-0
|
||||
.list-group-light.mb-3
|
||||
.list-group-item.d-flex.align-items-center(*ngFor='let fw of session.forwardedPorts')
|
||||
strong(*ngIf='fw.type === PortForwardType.Local') Local
|
||||
strong(*ngIf='fw.type === PortForwardType.Remote') Remote
|
||||
strong(*ngIf='fw.type === PortForwardType.Dynamic') Dynamic
|
||||
.ml-3 {{fw.host}}:{{fw.port}}
|
||||
.ml-2 →
|
||||
.ml-2(*ngIf='fw.type !== PortForwardType.Dynamic') {{fw.targetAddress}}:{{fw.targetPort}}
|
||||
.ml-2(*ngIf='fw.type === PortForwardType.Dynamic') SOCKS proxy
|
||||
button.btn.btn-link.ml-auto((click)='remove(fw)')
|
||||
i.fas.fa-trash-alt.mr-2
|
||||
span Remove
|
||||
|
||||
.input-group.mb-2(*ngIf='newForward.type === PortForwardType.Dynamic')
|
||||
input.form-control(type='text', [(ngModel)]='newForward.host')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.port')
|
||||
|
||||
.input-group.mb-2(*ngIf='newForward.type !== PortForwardType.Dynamic')
|
||||
input.form-control(type='text', [(ngModel)]='newForward.host')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.port')
|
||||
.input-group-append
|
||||
.input-group-text →
|
||||
input.form-control(type='text', [(ngModel)]='newForward.targetAddress')
|
||||
.input-group-append
|
||||
.input-group-text :
|
||||
input.form-control(type='number', [(ngModel)]='newForward.targetPort')
|
||||
|
||||
.d-flex
|
||||
.btn-group.mr-auto(
|
||||
[(ngModel)]='newForward.type',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Local'
|
||||
)
|
||||
| Local
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Remote'
|
||||
)
|
||||
| Remote
|
||||
label.btn.btn-secondary.m-0(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='PortForwardType.Dynamic'
|
||||
)
|
||||
| Dynamic
|
||||
|
||||
button.btn.btn-primary((click)='addForward()')
|
||||
i.fas.fa-check.mr-2
|
||||
span Forward port
|
||||
ssh-port-forwarding-config(
|
||||
[model]='session.forwardedPorts',
|
||||
(forwardAdded)='onForwardAdded($event)',
|
||||
(forwardRemoved)='onForwardRemoved($event)'
|
||||
)
|
||||
|
@@ -1,43 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ForwardedPort, PortForwardType, SSHSession } from '../api'
|
||||
import { ForwardedPort, ForwardedPortConfig, SSHSession } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./sshPortForwardingModal.component.pug'),
|
||||
// styles: [require('./sshPortForwardingModal.component.scss')],
|
||||
})
|
||||
export class SSHPortForwardingModalComponent {
|
||||
@Input() session: SSHSession
|
||||
newForward = new ForwardedPort()
|
||||
PortForwardType = PortForwardType
|
||||
|
||||
constructor (
|
||||
public modalInstance: NgbActiveModal,
|
||||
) {
|
||||
this.reset()
|
||||
onForwardAdded (fw: ForwardedPortConfig) {
|
||||
const newForward = new ForwardedPort()
|
||||
Object.assign(newForward, fw)
|
||||
this.session.addPortForward(newForward)
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.newForward = new ForwardedPort()
|
||||
this.newForward.type = PortForwardType.Local
|
||||
this.newForward.host = '127.0.0.1'
|
||||
this.newForward.port = 8000
|
||||
this.newForward.targetAddress = '127.0.0.1'
|
||||
this.newForward.targetPort = 80
|
||||
}
|
||||
|
||||
async addForward () {
|
||||
try {
|
||||
await this.session.addPortForward(this.newForward)
|
||||
this.reset()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
remove (fw: ForwardedPort) {
|
||||
this.session.removePortForward(fw)
|
||||
onForwardRemoved (fw: ForwardedPortConfig) {
|
||||
this.session.removePortForward(fw as ForwardedPort)
|
||||
}
|
||||
}
|
||||
|
@@ -1,33 +1,55 @@
|
||||
h3 Connections
|
||||
.d-flex.align-items-center.mb-3
|
||||
h3.m-0 SSH Connections
|
||||
|
||||
.list-group.list-group-flush.mt-3.mb-3
|
||||
button.btn.btn-primary.ml-auto((click)='createConnection()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span.ml-2 Add connection
|
||||
|
||||
.input-group.mb-3
|
||||
.input-group-prepend
|
||||
.input-group-text
|
||||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', placeholder='Filter', [(ngModel)]='filter')
|
||||
|
||||
.list-group.list-group-light.mt-3.mb-3
|
||||
ng-container(*ngFor='let group of childGroups')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
|
||||
)
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
|
||||
span.ml-3.mr-auto {{group.name || "Ungrouped"}}
|
||||
button.btn.btn-outline-info.ml-2((click)='editGroup(group)')
|
||||
i.fas.fa-edit
|
||||
button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)')
|
||||
i.fas.fa-trash
|
||||
ng-container(*ngIf='!groupCollapsed[group.name]')
|
||||
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
|
||||
*ngFor='let connection of group.connections',
|
||||
(click)='editConnection(connection)'
|
||||
ng-container(*ngIf='isGroupVisible(group)')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
|
||||
)
|
||||
.mr-auto
|
||||
div {{connection.name}}
|
||||
.text-muted {{connection.host}}
|
||||
button.btn.btn-outline-info.ml-1((click)='$event.stopPropagation(); copyConnection(connection)')
|
||||
i.fas.fa-copy
|
||||
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)')
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
|
||||
span.ml-3.mr-auto {{group.name || "Ungrouped"}}
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
|
||||
[class.invisible]='!group.name',
|
||||
(click)='$event.stopPropagation(); editGroup(group)'
|
||||
)
|
||||
i.fas.fa-edit
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
|
||||
[class.invisible]='!group.name',
|
||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
||||
)
|
||||
i.fas.fa-trash
|
||||
|
||||
button.btn.btn-primary((click)='createConnection()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span.ml-2 Add connection
|
||||
ng-container(*ngIf='!groupCollapsed[group.name]')
|
||||
ng-container(*ngFor='let connection of group.connections')
|
||||
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
|
||||
*ngIf='isConnectionVisible(connection)',
|
||||
(click)='editConnection(connection)'
|
||||
)
|
||||
.mr-3 {{connection.name}}
|
||||
.mr-auto.text-muted {{connection.host}}
|
||||
|
||||
.hover-reveal(ngbDropdown, placement='bottom-right')
|
||||
button.btn.btn-link(ngbDropdownToggle, (click)='$event.stopPropagation()')
|
||||
i.fas.fa-fw.fa-ellipsis-v
|
||||
div(ngbDropdownMenu)
|
||||
button.dropdown-item((click)='$event.stopPropagation(); copyConnection(connection)')
|
||||
i.fas.fa-copy
|
||||
span Duplicate
|
||||
button.dropdown-item((click)='$event.stopPropagation(); deleteConnection(connection)')
|
||||
i.fas.fa-trash
|
||||
span Delete
|
||||
|
||||
h3.mt-5 Options
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.list-group-item {
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
@@ -1,20 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import deepClone from 'clone-deep'
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { SSHConnection, SSHConnectionGroup } from '../api'
|
||||
import { SSHConnection } from '../api'
|
||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||
import { PromptModalComponent } from './promptModal.component'
|
||||
|
||||
interface SSHConnectionGroup {
|
||||
name: string|null
|
||||
connections: SSHConnection[]
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./sshSettingsTab.component.pug'),
|
||||
styles: [require('./sshSettingsTab.component.scss')],
|
||||
})
|
||||
export class SSHSettingsTabComponent {
|
||||
connections: SSHConnection[]
|
||||
childGroups: SSHConnectionGroup[]
|
||||
groupCollapsed: Record<string, boolean> = {}
|
||||
filter = ''
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
@@ -48,9 +56,10 @@ export class SSHSettingsTabComponent {
|
||||
|
||||
copyConnection (connection: SSHConnection) {
|
||||
const modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||
modal.componentInstance.connection = Object.assign({
|
||||
name: `${name} Copy`,
|
||||
}, connection)
|
||||
modal.componentInstance.connection = {
|
||||
...deepClone(connection),
|
||||
name: `${connection.name} Copy`,
|
||||
}
|
||||
modal.result.then(result => {
|
||||
this.connections.push(result)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
@@ -61,7 +70,7 @@ export class SSHSettingsTabComponent {
|
||||
|
||||
editConnection (connection: SSHConnection) {
|
||||
const modal = this.ngbModal.open(EditConnectionModalComponent, { size: 'lg' })
|
||||
modal.componentInstance.connection = Object.assign({}, connection)
|
||||
modal.componentInstance.connection = deepClone(connection)
|
||||
modal.result.then(result => {
|
||||
Object.assign(connection, result)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
@@ -131,7 +140,7 @@ export class SSHSettingsTabComponent {
|
||||
let group = this.childGroups.find(x => x.name === connection.group)
|
||||
if (!group) {
|
||||
group = {
|
||||
name: connection.group!,
|
||||
name: connection.group,
|
||||
connections: [],
|
||||
}
|
||||
this.childGroups.push(group)
|
||||
@@ -139,4 +148,12 @@ export class SSHSettingsTabComponent {
|
||||
group.connections.push(connection)
|
||||
}
|
||||
}
|
||||
|
||||
isGroupVisible (group: SSHConnectionGroup): boolean {
|
||||
return !this.filter || group.connections.some(x => this.isConnectionVisible(x))
|
||||
}
|
||||
|
||||
isConnectionVisible (connection: SSHConnection): boolean {
|
||||
return !this.filter || `${connection.name}$${connection.host}`.toLowerCase().includes(this.filter.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import { BaseTerminalTabComponent } from 'terminus-terminal'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHConnection, SSHSession } from '../api'
|
||||
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@@ -22,9 +21,15 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
connection?: SSHConnection
|
||||
session: SSHSession|null = null
|
||||
private sessionStack: SSHSession[] = []
|
||||
private homeEndSubscription: Subscription
|
||||
private recentInputs = ''
|
||||
private reconnectOffered = false
|
||||
private spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
stream: {
|
||||
write: x => this.write(x),
|
||||
},
|
||||
})
|
||||
private spinnerActive = false
|
||||
|
||||
constructor (
|
||||
injector: Injector,
|
||||
@@ -43,7 +48,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
this.enableDynamicTitle = !this.connection.disableDynamicTitle
|
||||
|
||||
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
@@ -87,13 +92,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
await this.setupOneSession(jumpSession)
|
||||
|
||||
this.attachSessionHandler(
|
||||
jumpSession.destroyed$.subscribe(() => {
|
||||
if (session.open) {
|
||||
session.destroy()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.attachSessionHandler(jumpSession.destroyed$, () => {
|
||||
if (session.open) {
|
||||
session.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
|
||||
'127.0.0.1', 0, session.connection.host, session.connection.port ?? 22,
|
||||
@@ -113,32 +116,22 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
this.sessionStack.push(session)
|
||||
}
|
||||
|
||||
this.attachSessionHandler(session.serviceMessage$.subscribe(msg => {
|
||||
this.write(`\r\n${colors.black.bgWhite(' SSH ')} ${msg}\r\n`)
|
||||
session.resize(this.size.columns, this.size.rows)
|
||||
}))
|
||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
|
||||
|
||||
this.startSpinner()
|
||||
|
||||
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
|
||||
|
||||
const spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
stream: {
|
||||
write: x => this.write(x),
|
||||
},
|
||||
this.attachSessionHandler(session.serviceMessage$, msg => {
|
||||
this.pauseSpinner(() => {
|
||||
this.write(`\r${colors.black.bgWhite(' SSH ')} ${msg}\r\n`)
|
||||
session.resize(this.size.columns, this.size.rows)
|
||||
})
|
||||
})
|
||||
spinner.setSpinnerString(6)
|
||||
spinner.start()
|
||||
|
||||
try {
|
||||
await this.ssh.connectSession(session, (message: string) => {
|
||||
spinner.stop(true)
|
||||
this.write(message + '\r\n')
|
||||
spinner.start()
|
||||
})
|
||||
spinner.stop(true)
|
||||
await this.ssh.connectSession(session)
|
||||
this.stopSpinner()
|
||||
} catch (e) {
|
||||
spinner.stop(true)
|
||||
this.stopSpinner()
|
||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||
return
|
||||
}
|
||||
@@ -146,7 +139,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
protected attachSessionHandlers (): void {
|
||||
const session = this.session!
|
||||
this.attachSessionHandler(session.destroyed$.subscribe(() => {
|
||||
this.attachSessionHandler(session.destroyed$, () => {
|
||||
if (
|
||||
// Ctrl-D
|
||||
this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 ||
|
||||
@@ -156,7 +149,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
this.destroy()
|
||||
} else {
|
||||
// Session was closed abruptly
|
||||
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
|
||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
|
||||
if (!this.reconnectOffered) {
|
||||
this.reconnectOffered = true
|
||||
this.write('Press any key to reconnect\r\n')
|
||||
@@ -167,7 +160,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
})
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
super.attachSessionHandlers()
|
||||
}
|
||||
|
||||
@@ -228,8 +221,23 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
)).response === 1
|
||||
}
|
||||
|
||||
ngOnDestroy (): void {
|
||||
this.homeEndSubscription.unsubscribe()
|
||||
super.ngOnDestroy()
|
||||
private startSpinner () {
|
||||
this.spinner.setSpinnerString(6)
|
||||
this.spinner.start()
|
||||
this.spinnerActive = true
|
||||
}
|
||||
|
||||
private stopSpinner () {
|
||||
this.spinner.stop(true)
|
||||
this.spinnerActive = false
|
||||
}
|
||||
|
||||
private pauseSpinner (work: () => void) {
|
||||
const wasActive = this.spinnerActive
|
||||
this.stopSpinner()
|
||||
work()
|
||||
if (wasActive) {
|
||||
this.startSpinner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import TerminusTerminalModule from 'terminus-terminal'
|
||||
|
||||
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
||||
import { SSHPortForwardingModalComponent } from './components/sshPortForwardingModal.component'
|
||||
import { SSHPortForwardingConfigComponent } from './components/sshPortForwardingConfig.component'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
@@ -49,6 +50,7 @@ import { WinSCPContextMenu } from './winSCPIntegration'
|
||||
EditConnectionModalComponent,
|
||||
PromptModalComponent,
|
||||
SSHPortForwardingModalComponent,
|
||||
SSHPortForwardingConfigComponent,
|
||||
SSHSettingsTabComponent,
|
||||
SSHTabComponent,
|
||||
],
|
||||
|
@@ -6,16 +6,17 @@ import { SSHTabComponent } from './components/sshTab.component'
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
|
||||
if (recoveryToken.type === 'app:ssh-tab') {
|
||||
return {
|
||||
type: SSHTabComponent,
|
||||
options: {
|
||||
connection: recoveryToken['connection'],
|
||||
savedState: recoveryToken['savedState'],
|
||||
},
|
||||
}
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:ssh-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
|
||||
return {
|
||||
type: SSHTabComponent,
|
||||
options: {
|
||||
connection: recoveryToken['connection'],
|
||||
savedState: recoveryToken['savedState'],
|
||||
},
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@@ -5,26 +5,44 @@ import * as keytar from 'keytar'
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PasswordStorageService {
|
||||
async savePassword (connection: SSHConnection, password: string): Promise<void> {
|
||||
let key = `ssh@${connection.host}`
|
||||
if (connection.port) {
|
||||
key = `ssh@${connection.host}:${connection.port}`
|
||||
}
|
||||
const key = this.getKeyForConnection(connection)
|
||||
return keytar.setPassword(key, connection.user, password)
|
||||
}
|
||||
|
||||
async deletePassword (connection: SSHConnection): Promise<void> {
|
||||
let key = `ssh@${connection.host}`
|
||||
if (connection.port) {
|
||||
key = `ssh@${connection.host}:${connection.port}`
|
||||
}
|
||||
const key = this.getKeyForConnection(connection)
|
||||
await keytar.deletePassword(key, connection.user)
|
||||
}
|
||||
|
||||
async loadPassword (connection: SSHConnection): Promise<string|null> {
|
||||
const key = this.getKeyForConnection(connection)
|
||||
return keytar.getPassword(key, connection.user)
|
||||
}
|
||||
|
||||
async savePrivateKeyPassword (id: string, password: string): Promise<void> {
|
||||
const key = this.getKeyForPrivateKey(id)
|
||||
return keytar.setPassword(key, 'user', password)
|
||||
}
|
||||
|
||||
async deletePrivateKeyPassword (id: string): Promise<void> {
|
||||
const key = this.getKeyForPrivateKey(id)
|
||||
await keytar.deletePassword(key, 'user')
|
||||
}
|
||||
|
||||
async loadPrivateKeyPassword (id: string): Promise<string|null> {
|
||||
const key = this.getKeyForPrivateKey(id)
|
||||
return keytar.getPassword(key, 'user')
|
||||
}
|
||||
|
||||
private getKeyForConnection (connection: SSHConnection): string {
|
||||
let key = `ssh@${connection.host}`
|
||||
if (connection.port) {
|
||||
key = `ssh@${connection.host}:${connection.port}`
|
||||
}
|
||||
return keytar.getPassword(key, connection.user)
|
||||
return key
|
||||
}
|
||||
|
||||
private getKeyForPrivateKey (id: string): string {
|
||||
return `ssh-private-key:${id}`
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import colors from 'ansi-colors'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { Duplex } from 'stream'
|
||||
import * as crypto from 'crypto'
|
||||
import { open as openTemp } from 'temp'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -7,14 +8,17 @@ import { Client } from 'ssh2'
|
||||
import { SSH2Stream } from 'ssh2-streams'
|
||||
import * as fs from 'mz/fs'
|
||||
import { execFile } from 'mz/child_process'
|
||||
import { exec } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import * as sshpk from 'sshpk'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { HostAppService, Platform, Logger, LogService, ElectronService, AppService, SelectorOption, ConfigService, NotificationsService } from 'terminus-core'
|
||||
import { SettingsTabComponent } from 'terminus-settings'
|
||||
import { ALGORITHM_BLACKLIST, SSHConnection, SSHSession } from '../api'
|
||||
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHConnection, SSHSession } from '../api'
|
||||
import { PromptModalComponent } from '../components/promptModal.component'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
import { SSHTabComponent } from '../components/sshTab.component'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
@@ -51,51 +55,29 @@ export class SSHService {
|
||||
return session
|
||||
}
|
||||
|
||||
async loadPrivateKeyForSession (session: SSHSession, logCallback?: SSHLogCallback): Promise<string|null> {
|
||||
async loadPrivateKeyForSession (session: SSHSession): Promise<string|null> {
|
||||
let privateKey: string|null = null
|
||||
let privateKeyPath = session.connection.privateKey
|
||||
|
||||
if (!privateKeyPath) {
|
||||
const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
logCallback?.('Using user\'s default private key')
|
||||
session.emitServiceMessage('Using user\'s default private key')
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyPath) {
|
||||
logCallback?.('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||
session.emitServiceMessage('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||
try {
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
logCallback?.(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
||||
session.emitServiceMessage(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
||||
this.notifications.error('Could not read the private key file')
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
let parsedKey: any = null
|
||||
try {
|
||||
parsedKey = sshpk.parsePrivateKey(privateKey, 'auto')
|
||||
} catch (e) {
|
||||
if (e instanceof sshpk.KeyEncryptedError) {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
logCallback?.(colors.bgYellow.yellow.black(' ! ') + ' Key requires passphrase')
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
let passphrase = ''
|
||||
try {
|
||||
const result = await modal.result
|
||||
passphrase = result?.value
|
||||
} catch { }
|
||||
parsedKey = sshpk.parsePrivateKey(
|
||||
privateKey,
|
||||
'auto',
|
||||
{ passphrase: passphrase }
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
const parsedKey = await this.parsePrivateKey(privateKey)
|
||||
|
||||
const sshFormatKey = parsedKey.toString('openssh')
|
||||
const temp = await openTemp()
|
||||
@@ -130,15 +112,40 @@ export class SSHService {
|
||||
return privateKey
|
||||
}
|
||||
|
||||
async connectSession (session: SSHSession, logCallback?: SSHLogCallback): Promise<void> {
|
||||
if (!logCallback) {
|
||||
logCallback = () => null
|
||||
}
|
||||
async parsePrivateKey (privateKey: string): Promise<any> {
|
||||
const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
|
||||
let passphrase: string|null = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
|
||||
while (true) {
|
||||
try {
|
||||
return sshpk.parsePrivateKey(privateKey, 'auto', { passphrase })
|
||||
} catch (e) {
|
||||
this.notifications.error('Could not read the private key', e.toString())
|
||||
if (e instanceof sshpk.KeyEncryptedError || e instanceof sshpk.KeyParseError) {
|
||||
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
|
||||
|
||||
const log = (s: any) => {
|
||||
logCallback!(s)
|
||||
this.logger.info(stripAnsi(s))
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
modal.componentInstance.showRememberCheckbox = true
|
||||
|
||||
try {
|
||||
const result = await modal.result
|
||||
passphrase = result?.value
|
||||
if (passphrase && result.remember) {
|
||||
this.passwordStorage.savePrivateKeyPassword(keyHash, passphrase)
|
||||
}
|
||||
} catch {
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectSession (session: SSHSession): Promise<void> {
|
||||
const log = (s: any) => session.emitServiceMessage(s)
|
||||
|
||||
let privateKey: string|null = null
|
||||
|
||||
@@ -150,12 +157,18 @@ export class SSHService {
|
||||
for (const key of Object.keys(session.connection.algorithms ?? {})) {
|
||||
algorithms[key] = session.connection.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
||||
}
|
||||
await new Promise(async (resolve, reject) => {
|
||||
|
||||
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
||||
ssh.on('ready', () => {
|
||||
connected = true
|
||||
if (savedPassword) {
|
||||
this.passwordStorage.savePassword(session.connection, savedPassword)
|
||||
}
|
||||
|
||||
for (const fw of session.connection.forwardedPorts ?? []) {
|
||||
session.addPortForward(Object.assign(new ForwardedPort(), fw))
|
||||
}
|
||||
|
||||
this.zone.run(resolve)
|
||||
})
|
||||
ssh.on('error', error => {
|
||||
@@ -207,126 +220,143 @@ export class SSHService {
|
||||
log(banner)
|
||||
}
|
||||
})
|
||||
|
||||
let agent: string|null = null
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
||||
agent = WINDOWS_OPENSSH_AGENT_PIPE
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const pageantRunning = await new Promise<boolean>(resolve => {
|
||||
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
|
||||
resolve(list.some(x => x.name === 'pageant.exe'))
|
||||
}, 0)
|
||||
})
|
||||
if (pageantRunning) {
|
||||
agent = 'pageant'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
agent = process.env.SSH_AUTH_SOCK!
|
||||
}
|
||||
|
||||
const authMethodsLeft = ['none']
|
||||
if (!session.connection.auth || session.connection.auth === 'publicKey') {
|
||||
privateKey = await this.loadPrivateKeyForSession(session, log)
|
||||
if (!privateKey) {
|
||||
log('\r\nPrivate key auth selected, but no key is loaded\r\n')
|
||||
} else {
|
||||
authMethodsLeft.push('publickey')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'agent') {
|
||||
if (!agent) {
|
||||
log('\r\nAgent auth selected, but no running agent is detected\r\n')
|
||||
} else {
|
||||
authMethodsLeft.push('agent')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'password') {
|
||||
authMethodsLeft.push('password')
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
|
||||
authMethodsLeft.push('keyboard-interactive')
|
||||
}
|
||||
authMethodsLeft.push('hostbased')
|
||||
|
||||
try {
|
||||
ssh.connect({
|
||||
host: session.connection.host,
|
||||
port: session.connection.port ?? 22,
|
||||
username: session.connection.user,
|
||||
password: session.connection.privateKey ? undefined : '',
|
||||
privateKey: privateKey ?? undefined,
|
||||
tryKeyboard: true,
|
||||
agent: agent ?? undefined,
|
||||
agentForward: session.connection.agentForward && !!agent,
|
||||
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||
readyTimeout: session.connection.readyTimeout,
|
||||
hostVerifier: (digest: string) => {
|
||||
log(colors.bgWhite(' ') + ' Host key fingerprint:')
|
||||
log(colors.bgWhite(' ') + ' ' + colors.black.bgWhite(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||
return true
|
||||
},
|
||||
hostHash: 'sha256' as any,
|
||||
algorithms,
|
||||
sock: session.jumpStream,
|
||||
authHandler: methodsLeft => {
|
||||
while (true) {
|
||||
const method = authMethodsLeft.shift()
|
||||
if (!method) {
|
||||
return false
|
||||
}
|
||||
if (methodsLeft && !methodsLeft.includes(method) && method !== 'agent') {
|
||||
// Agent can still be used even if not in methodsLeft
|
||||
this.logger.info('Server does not support auth method', method)
|
||||
continue
|
||||
}
|
||||
return method
|
||||
}
|
||||
},
|
||||
} as any)
|
||||
} catch (e) {
|
||||
this.notifications.error(e.message)
|
||||
return reject(e)
|
||||
}
|
||||
|
||||
let keychainPasswordUsed = false
|
||||
|
||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||
if (session.connection.password) {
|
||||
log('Using preset password')
|
||||
return session.connection.password
|
||||
}
|
||||
|
||||
if (!keychainPasswordUsed) {
|
||||
const password = await this.passwordStorage.loadPassword(session.connection)
|
||||
if (password) {
|
||||
log('Trying saved password')
|
||||
keychainPasswordUsed = true
|
||||
return password
|
||||
}
|
||||
}
|
||||
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
|
||||
modal.componentInstance.password = true
|
||||
modal.componentInstance.showRememberCheckbox = true
|
||||
try {
|
||||
const result = await modal.result
|
||||
if (result) {
|
||||
if (result.remember) {
|
||||
savedPassword = result.value
|
||||
}
|
||||
return result.value
|
||||
}
|
||||
return ''
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let agent: string|null = null
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
||||
agent = WINDOWS_OPENSSH_AGENT_PIPE
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const pageantRunning = await new Promise<boolean>(resolve => {
|
||||
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
|
||||
resolve(list.some(x => x.name === 'pageant.exe'))
|
||||
}, 0)
|
||||
})
|
||||
if (pageantRunning) {
|
||||
agent = 'pageant'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
agent = process.env.SSH_AUTH_SOCK!
|
||||
}
|
||||
|
||||
const authMethodsLeft = ['none']
|
||||
if (!session.connection.auth || session.connection.auth === 'publicKey') {
|
||||
try {
|
||||
privateKey = await this.loadPrivateKeyForSession(session)
|
||||
} catch (e) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key: ${e}`)
|
||||
}
|
||||
if (!privateKey) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Private key auth selected, but no key is loaded`)
|
||||
} else {
|
||||
authMethodsLeft.push('publickey')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'agent') {
|
||||
if (!agent) {
|
||||
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running agent is detected`)
|
||||
} else {
|
||||
authMethodsLeft.push('agent')
|
||||
}
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'password') {
|
||||
authMethodsLeft.push('password')
|
||||
}
|
||||
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
|
||||
authMethodsLeft.push('keyboard-interactive')
|
||||
}
|
||||
authMethodsLeft.push('hostbased')
|
||||
|
||||
try {
|
||||
if (session.connection.proxyCommand) {
|
||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${session.connection.proxyCommand}`)
|
||||
session.proxyCommandStream = new ProxyCommandStream(session.connection.proxyCommand)
|
||||
|
||||
session.proxyCommandStream.output$.subscribe((message: string) => {
|
||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message.trim())
|
||||
})
|
||||
|
||||
await session.proxyCommandStream.start()
|
||||
}
|
||||
|
||||
ssh.connect({
|
||||
host: session.connection.host,
|
||||
port: session.connection.port ?? 22,
|
||||
sock: session.proxyCommandStream ?? session.jumpStream,
|
||||
username: session.connection.user,
|
||||
password: session.connection.privateKey ? undefined : '',
|
||||
privateKey: privateKey ?? undefined,
|
||||
tryKeyboard: true,
|
||||
agent: agent ?? undefined,
|
||||
agentForward: session.connection.agentForward && !!agent,
|
||||
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||
readyTimeout: session.connection.readyTimeout,
|
||||
hostVerifier: (digest: string) => {
|
||||
log('Host key fingerprint:')
|
||||
log(colors.white.bgBlack(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||
return true
|
||||
},
|
||||
hostHash: 'sha256' as any,
|
||||
algorithms,
|
||||
authHandler: methodsLeft => {
|
||||
while (true) {
|
||||
const method = authMethodsLeft.shift()
|
||||
if (!method) {
|
||||
return false
|
||||
}
|
||||
if (methodsLeft && !methodsLeft.includes(method) && method !== 'agent') {
|
||||
// Agent can still be used even if not in methodsLeft
|
||||
this.logger.info('Server does not support auth method', method)
|
||||
continue
|
||||
}
|
||||
return method
|
||||
}
|
||||
},
|
||||
} as any)
|
||||
} catch (e) {
|
||||
this.notifications.error(e.message)
|
||||
throw e
|
||||
}
|
||||
|
||||
let keychainPasswordUsed = false
|
||||
|
||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||
if (session.connection.password) {
|
||||
log('Using preset password')
|
||||
return session.connection.password
|
||||
}
|
||||
|
||||
if (!keychainPasswordUsed) {
|
||||
const password = await this.passwordStorage.loadPassword(session.connection)
|
||||
if (password) {
|
||||
log('Trying saved password')
|
||||
keychainPasswordUsed = true
|
||||
return password
|
||||
}
|
||||
}
|
||||
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
|
||||
modal.componentInstance.password = true
|
||||
modal.componentInstance.showRememberCheckbox = true
|
||||
try {
|
||||
const result = await modal.result
|
||||
if (result) {
|
||||
if (result.remember) {
|
||||
savedPassword = result.value
|
||||
}
|
||||
return result.value
|
||||
}
|
||||
return ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
return resultPromise
|
||||
}
|
||||
|
||||
async showConnectionSelector (): Promise<void> {
|
||||
@@ -448,6 +478,52 @@ export class SSHService {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProxyCommandStream extends Duplex {
|
||||
private process: ChildProcess
|
||||
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
private output = new Subject<string>()
|
||||
|
||||
constructor (private command: string) {
|
||||
super({
|
||||
allowHalfOpen: false,
|
||||
})
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.process = exec(this.command, {
|
||||
windowsHide: true,
|
||||
encoding: 'buffer',
|
||||
})
|
||||
this.process.on('exit', code => {
|
||||
this.destroy(new Error(`Proxy command has exited with code ${code}`))
|
||||
})
|
||||
this.process.stdout?.on('data', data => {
|
||||
this.push(data)
|
||||
})
|
||||
this.process.stdout?.on('error', (err) => {
|
||||
this.destroy(err)
|
||||
})
|
||||
this.process.stderr?.on('data', data => {
|
||||
this.output.next(data.toString())
|
||||
})
|
||||
}
|
||||
|
||||
_read (size: number): void {
|
||||
process.stdout.read(size)
|
||||
}
|
||||
|
||||
_write (chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
||||
this.process.stdin?.write(chunk, callback)
|
||||
}
|
||||
|
||||
_destroy (error: Error|null, callback: (error: Error|null) => void): void {
|
||||
this.process.kill()
|
||||
this.output.complete()
|
||||
callback(error)
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
const _authPassword = SSH2Stream.prototype.authPassword
|
||||
SSH2Stream.prototype.authPassword = async function (username, passwordFn: any) {
|
||||
|
@@ -27,10 +27,10 @@ ansi-colors@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
|
||||
|
||||
ansi-regex@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
||||
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
||||
ansi-regex@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.0.tgz#ecc7f5933cbe5ac7b33e209a5ff409ab1669c6b2"
|
||||
integrity sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==
|
||||
|
||||
asn1@~0.2.0, asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
@@ -90,6 +90,15 @@ cliff@0.1.x:
|
||||
eyes "~0.1.8"
|
||||
winston "0.8.x"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
|
||||
dependencies:
|
||||
is-plain-object "^2.0.4"
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
colors@0.6.x:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
|
||||
@@ -176,6 +185,18 @@ ipv6@*:
|
||||
cliff "0.1.x"
|
||||
sprintf "0.1.x"
|
||||
|
||||
is-plain-object@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
|
||||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
isobject@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isstream@0.1.x:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -186,6 +207,11 @@ jsbn@~0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
||||
kind-of@^6.0.2:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
@@ -230,15 +256,22 @@ rimraf@~2.6.2:
|
||||
glob "^7.1.3"
|
||||
|
||||
run-script-os@^1.1.3:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.5.tgz#6038a92b370a1cbca5fa2fa73d1c740b0037b6ff"
|
||||
integrity sha512-kLvUnf1SM1+gSH2bY1P41MNFaWdXaG2nkhoJS9WQNQxk6g9KNuncKderBLG7vOsp77GtQqNs0bEazxip/I6wfg==
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347"
|
||||
integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
|
||||
|
||||
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
shallow-clone@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
|
||||
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
|
||||
dependencies:
|
||||
kind-of "^6.0.2"
|
||||
|
||||
socksv5@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/socksv5/-/socksv5-0.0.6.tgz#1327235ff7e8de21ac434a0a579dc69c3f071061"
|
||||
@@ -300,12 +333,12 @@ streamsearch@~0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
||||
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
|
||||
|
||||
strip-ansi@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
|
||||
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
|
||||
strip-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.0.tgz#1dc49b980c3a4100366617adac59327eefdefcb0"
|
||||
integrity sha512-UhDTSnGF1dc0DRbUqr1aXwNoY3RgVkSWG8BrpnuFIxhP57IqbS7IRta2Gfiavds4yCxc5+fEAVVOgBZWnYkvzg==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
ansi-regex "^6.0.0"
|
||||
|
||||
temp@^0.9.1:
|
||||
version "0.9.4"
|
||||
|
@@ -16,23 +16,27 @@
|
||||
],
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"opentype.js": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/deep-equal": "^1.0.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"dataurl": "0.1.0",
|
||||
"deep-equal": "1.1.0",
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"mz": "^2.6.0",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"slugify": "^1.4.0",
|
||||
"utils-decorators": "^1.8.1",
|
||||
"xterm": "^4.9.0-beta.7",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-ligatures": "^0.4.0",
|
||||
"xterm-addon-ligatures": "^0.5.0",
|
||||
"xterm-addon-search": "^0.8.0",
|
||||
"xterm-addon-serialize": "^0.5.0",
|
||||
"xterm-addon-unicode11": "^0.2.0",
|
||||
"xterm-addon-webgl": "^0.10.0",
|
||||
"xterm-addon-webgl": "^0.11.0",
|
||||
"zmodem.js": "^0.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -4,7 +4,7 @@ import { first } from 'rxjs/operators'
|
||||
import colors from 'ansi-colors'
|
||||
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core'
|
||||
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent } from 'terminus-core'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer } from 'terminus-core'
|
||||
|
||||
import { BaseSession, SessionsService } from '../services/sessions.service'
|
||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||
@@ -22,16 +22,26 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
static styles: string[] = [require<string>('../components/terminalTab.component.scss')]
|
||||
static animations: AnimationTriggerMetadata[] = [trigger('slideInOut', [
|
||||
transition(':enter', [
|
||||
style({ transform: 'translateY(-25%)' }),
|
||||
animate('100ms ease-in-out', style({ transform: 'translateY(0%)' })),
|
||||
style({
|
||||
transform: 'translateY(-25%)',
|
||||
opacity: '0',
|
||||
}),
|
||||
animate('100ms ease-out', style({
|
||||
transform: 'translateY(0%)',
|
||||
opacity: '1',
|
||||
})),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('100ms ease-in-out', style({ transform: 'translateY(-25%)' })),
|
||||
animate('100ms ease-out', style({
|
||||
transform: 'translateY(-25%)',
|
||||
opacity: '0',
|
||||
})),
|
||||
]),
|
||||
])]
|
||||
|
||||
session: BaseSession|null = null
|
||||
savedState?: any
|
||||
savedStateIsLive = false
|
||||
|
||||
@Input() zoom = 0
|
||||
|
||||
@@ -85,12 +95,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
protected logger: Logger
|
||||
protected output = new Subject<string>()
|
||||
protected sessionChanged = new Subject<BaseSession|null>()
|
||||
private sessionCloseSubscription: Subscription
|
||||
private hotkeysSubscription: Subscription
|
||||
private bellPlayer: HTMLAudioElement
|
||||
private termContainerSubscriptions: Subscription[] = []
|
||||
private termContainerSubscriptions = new SubscriptionContainer()
|
||||
private allFocusModeSubscription: Subscription|null = null
|
||||
private sessionHandlers: Subscription[] = []
|
||||
private sessionHandlers = new SubscriptionContainer()
|
||||
|
||||
get input$ (): Observable<Buffer> {
|
||||
if (!this.frontend) {
|
||||
@@ -139,7 +147,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.logger = this.log.create('baseTerminalTab')
|
||||
this.setTitle('Terminal')
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(async hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, async hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
@@ -161,6 +169,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
case 'paste':
|
||||
this.paste()
|
||||
break
|
||||
case 'select-all':
|
||||
this.frontend?.selectAll()
|
||||
break
|
||||
case 'clear':
|
||||
this.frontend?.clear()
|
||||
break
|
||||
@@ -212,7 +223,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
}
|
||||
})
|
||||
this.bellPlayer = document.createElement('audio')
|
||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||
this.bellPlayer.src = require('../bell.ogg').default
|
||||
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
@@ -226,8 +237,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
|
||||
this.frontend = this.terminalContainersService.getFrontend(this.session)
|
||||
|
||||
this.frontend.ready$.subscribe(() => {
|
||||
this.frontendIsReady = true
|
||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||
this.onFrontendReady()
|
||||
})
|
||||
|
||||
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
|
||||
@@ -253,20 +264,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.alternateScreenActive = x
|
||||
})
|
||||
|
||||
if (this.savedState) {
|
||||
this.frontend.restoreState(this.savedState)
|
||||
this.frontend.write('\r\n\r\n')
|
||||
this.frontend.write(colors.bgWhite.black(' * ') + colors.bgBlackBright.white(' History restored '))
|
||||
this.frontend.write('\r\n\r\n')
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
setImmediate(async () => {
|
||||
if (this.hasFocus) {
|
||||
this.frontend!.attach(this.content.nativeElement)
|
||||
await this.frontend!.attach(this.content.nativeElement)
|
||||
this.frontend!.configure()
|
||||
} else {
|
||||
this.focused$.pipe(first()).subscribe(() => {
|
||||
this.frontend!.attach(this.content.nativeElement)
|
||||
this.focused$.pipe(first()).subscribe(async () => {
|
||||
await this.frontend!.attach(this.content.nativeElement)
|
||||
this.frontend!.configure()
|
||||
})
|
||||
}
|
||||
@@ -298,6 +302,18 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
})
|
||||
}
|
||||
|
||||
protected onFrontendReady (): void {
|
||||
this.frontendIsReady = true
|
||||
if (this.savedState) {
|
||||
this.frontend!.restoreState(this.savedState)
|
||||
if (!this.savedStateIsLive) {
|
||||
this.frontend!.write('\r\n\r\n')
|
||||
this.frontend!.write(colors.bgWhite.black(' * ') + colors.bgBlackBright.white(' History restored '))
|
||||
this.frontend!.write('\r\n\r\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<MenuItemConstructorOptions[]> {
|
||||
let items: MenuItemConstructorOptions[] = []
|
||||
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
|
||||
@@ -329,15 +345,16 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
throw new Error('Frontend not ready')
|
||||
}
|
||||
|
||||
const percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
||||
if (!this.alternateScreenActive && percentageMatch && this.config.store.terminal.detectProgress) {
|
||||
const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||
if (percentage > 0 && percentage <= 100) {
|
||||
this.setProgress(percentage)
|
||||
// this.logger.debug('Detected progress:', percentage)
|
||||
if (this.config.store.terminal.detectProgress) {
|
||||
const percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
||||
if (!this.alternateScreenActive && percentageMatch) {
|
||||
const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||
if (percentage > 0 && percentage <= 100) {
|
||||
this.setProgress(percentage)
|
||||
}
|
||||
} else {
|
||||
this.setProgress(null)
|
||||
}
|
||||
} else {
|
||||
this.setProgress(null)
|
||||
}
|
||||
this.frontend.write(data)
|
||||
}
|
||||
@@ -456,7 +473,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
|
||||
/** @hidden */
|
||||
ngOnDestroy (): void {
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
this.frontend?.detach(this.content.nativeElement)
|
||||
this.frontend = undefined
|
||||
this.content.nativeElement.remove()
|
||||
this.detachTermContainerHandlers()
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
try {
|
||||
@@ -465,14 +488,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.logger.warn('Decorator attach() throws', e)
|
||||
}
|
||||
})
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
if (this.sessionCloseSubscription) {
|
||||
this.sessionCloseSubscription.unsubscribe()
|
||||
}
|
||||
this.output.complete()
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
super.destroy()
|
||||
if (this.session?.open) {
|
||||
await this.session.destroy()
|
||||
@@ -480,10 +497,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
}
|
||||
|
||||
protected detachTermContainerHandlers (): void {
|
||||
for (const subscription of this.termContainerSubscriptions) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
this.termContainerSubscriptions = []
|
||||
this.termContainerSubscriptions.cancelAll()
|
||||
}
|
||||
|
||||
protected attachTermContainerHandlers (): void {
|
||||
@@ -499,71 +513,69 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
}
|
||||
}
|
||||
|
||||
this.termContainerSubscriptions = [
|
||||
this.frontend.title$.subscribe(title => this.zone.run(() => {
|
||||
if (this.enableDynamicTitle) {
|
||||
this.setTitle(title)
|
||||
this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => {
|
||||
if (this.enableDynamicTitle) {
|
||||
this.setTitle(title)
|
||||
}
|
||||
}))
|
||||
|
||||
this.termContainerSubscriptions.subscribe(this.focused$, () => this.frontend && (this.frontend.enableResizing = true))
|
||||
this.termContainerSubscriptions.subscribe(this.blurred$, () => this.frontend && (this.frontend.enableResizing = false))
|
||||
|
||||
this.termContainerSubscriptions.subscribe(this.frontend.mouseEvent$, async event => {
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 2) {
|
||||
if (this.config.store.terminal.pasteOnMiddleClick) {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
})),
|
||||
|
||||
this.focused$.subscribe(() => this.frontend && (this.frontend.enableResizing = true)),
|
||||
this.blurred$.subscribe(() => this.frontend && (this.frontend.enableResizing = false)),
|
||||
|
||||
this.frontend.mouseEvent$.subscribe(async event => {
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 2) {
|
||||
if (this.config.store.terminal.pasteOnMiddleClick) {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.which === 3 || event.which === 1 && event.ctrlKey) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.hostApp.popupContextMenu(await this.buildContextMenu())
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
if (event.which === 3 || event.which === 1 && event.ctrlKey) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.hostApp.popupContextMenu(await this.buildContextMenu())
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.type === 'mousewheel') {
|
||||
let wheelDeltaY = 0
|
||||
}
|
||||
if (event.type === 'mousewheel') {
|
||||
let wheelDeltaY = 0
|
||||
|
||||
if ('wheelDeltaY' in event) {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
||||
} else {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
event.preventDefault()
|
||||
const delta = Math.round(wheelDeltaY / 50)
|
||||
this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
if ('wheelDeltaY' in event) {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
||||
} else {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
||||
}
|
||||
}),
|
||||
|
||||
this.frontend.input$.subscribe(data => {
|
||||
this.sendInput(data)
|
||||
}),
|
||||
if (event.altKey) {
|
||||
event.preventDefault()
|
||||
const delta = Math.round(wheelDeltaY / 50)
|
||||
this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.frontend.resize$.subscribe(({ columns, rows }) => {
|
||||
this.logger.debug(`Resizing to ${columns}x${rows}`)
|
||||
this.size = { columns, rows }
|
||||
this.zone.run(() => {
|
||||
if (this.session?.open) {
|
||||
this.session.resize(columns, rows)
|
||||
}
|
||||
})
|
||||
}),
|
||||
this.termContainerSubscriptions.subscribe(this.frontend.input$, data => {
|
||||
this.sendInput(data)
|
||||
})
|
||||
|
||||
this.hostApp.displayMetricsChanged$.subscribe(maybeConfigure),
|
||||
this.hostApp.windowMoved$.subscribe(maybeConfigure),
|
||||
]
|
||||
this.termContainerSubscriptions.subscribe(this.frontend.resize$, ({ columns, rows }) => {
|
||||
this.logger.debug(`Resizing to ${columns}x${rows}`)
|
||||
this.size = { columns, rows }
|
||||
this.zone.run(() => {
|
||||
if (this.session?.open) {
|
||||
this.session.resize(columns, rows)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.termContainerSubscriptions.subscribe(this.hostApp.displayMetricsChanged$, maybeConfigure)
|
||||
this.termContainerSubscriptions.subscribe(this.hostApp.windowMoved$, maybeConfigure)
|
||||
}
|
||||
|
||||
setSession (session: BaseSession|null, destroyOnSessionClose = false): void {
|
||||
@@ -581,8 +593,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
this.sessionChanged.next(session)
|
||||
}
|
||||
|
||||
protected attachSessionHandler (subscription: Subscription): void {
|
||||
this.sessionHandlers.push(subscription)
|
||||
protected attachSessionHandler <T> (observable: Observable<T>, handler: (v: T) => void): void {
|
||||
this.sessionHandlers.subscribe(observable, handler)
|
||||
}
|
||||
|
||||
protected attachSessionHandlers (destroyOnSessionClose = false): void {
|
||||
@@ -591,31 +603,26 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
||||
}
|
||||
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.attachSessionHandler(this.session.output$.subscribe(data => {
|
||||
this.attachSessionHandler(this.session.output$, data => {
|
||||
if (this.enablePassthrough) {
|
||||
this.zone.run(() => {
|
||||
this.output.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
this.output.next(data)
|
||||
this.write(data)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
if (destroyOnSessionClose) {
|
||||
this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.attachSessionHandler(this.session.closed$, () => {
|
||||
this.frontend?.destroy()
|
||||
this.destroy()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
this.attachSessionHandler(this.session.destroyed$.subscribe(() => {
|
||||
this.attachSessionHandler(this.session.destroyed$, () => {
|
||||
this.setSession(null)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
protected detachSessionHandlers (): void {
|
||||
for (const s of this.sessionHandlers) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
this.sessionHandlers = []
|
||||
this.sessionHandlers.cancelAll()
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ export interface ResizeEvent {
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
restoreFromPTYID?: string
|
||||
name?: string
|
||||
command: string
|
||||
args?: string[]
|
||||
@@ -53,3 +54,9 @@ export interface Shell {
|
||||
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export interface ChildProcess {
|
||||
pid: number
|
||||
ppid: number
|
||||
command: string
|
||||
}
|
||||
|
@@ -1,57 +0,0 @@
|
||||
/** @hidden */
|
||||
module.exports = function patchPTYModule (mod) {
|
||||
const oldSpawn = mod.spawn
|
||||
if (mod.patched) {
|
||||
return
|
||||
}
|
||||
mod.patched = true
|
||||
mod.spawn = (file, args, opt) => {
|
||||
let terminal = oldSpawn(file, args, opt)
|
||||
let timeout = null
|
||||
let buffer = Buffer.from('')
|
||||
let lastFlush = 0
|
||||
let nextTimeout = 0
|
||||
|
||||
// Minimum prebuffering window (ms) if the input is non-stop flowing
|
||||
const minWindow = 10
|
||||
|
||||
// Maximum buffering time (ms) until output must be flushed unconditionally
|
||||
const maxWindow = 100
|
||||
|
||||
function flush () {
|
||||
if (buffer.length) {
|
||||
terminal.emit('data-buffered', buffer)
|
||||
}
|
||||
lastFlush = Date.now()
|
||||
buffer = Buffer.from('')
|
||||
}
|
||||
|
||||
function reschedule () {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
nextTimeout = Date.now() + minWindow
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
flush()
|
||||
}, minWindow)
|
||||
}
|
||||
|
||||
terminal.on('data', data => {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data)
|
||||
}
|
||||
buffer = Buffer.concat([buffer, data])
|
||||
if (Date.now() - lastFlush > maxWindow) {
|
||||
// Taking too much time buffering, flush to keep things interactive
|
||||
flush()
|
||||
} else {
|
||||
if (Date.now() > nextTimeout - maxWindow / 10) {
|
||||
// Extend the window if it's expiring
|
||||
reschedule()
|
||||
}
|
||||
}
|
||||
})
|
||||
return terminal
|
||||
}
|
||||
}
|
@@ -15,10 +15,10 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
private terminal: TerminalService,
|
||||
) {
|
||||
super()
|
||||
if (!electron.remote.process.env.TERMINUS_DEV) {
|
||||
if (!electron.process.env.TERMINUS_DEV) {
|
||||
setImmediate(async () => {
|
||||
const argv: string[] = electron.remote.process.argv
|
||||
for (const arg of argv.slice(1).concat([electron.remote.process.argv0])) {
|
||||
const argv: string[] = electron.process.argv
|
||||
for (const arg of argv.slice(1).concat([electron.process.argv0])) {
|
||||
if (await fs.exists(arg)) {
|
||||
if ((await fs.stat(arg)).isDirectory()) {
|
||||
this.terminal.openTab(undefined, arg)
|
||||
|
@@ -33,19 +33,6 @@ h3.mb-3 Appearance
|
||||
.col-12.col-md-6
|
||||
color-scheme-preview([scheme]='config.store.terminal.colorScheme', [fontPreview]='true')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Frontend
|
||||
.description Switches terminal frontend implementation (experimental)
|
||||
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.terminal.frontend',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(value='hterm') hterm
|
||||
option(value='xterm') xterm
|
||||
option(value='xterm-webgl') xterm (WebGL)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Terminal background
|
||||
@@ -110,24 +97,6 @@ h3.mb-3 Appearance
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Hide tab index
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.terminal.hideTabIndex',
|
||||
(ngModelChange)='config.save();',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Hide tab close button
|
||||
|
||||
toggle(
|
||||
[(ngModel)]='config.store.terminal.hideCloseButton',
|
||||
(ngModelChange)='config.save();',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Fallback font
|
||||
@@ -139,3 +108,13 @@ h3.mb-3 Appearance
|
||||
[(ngModel)]='config.store.terminal.fallbackFont',
|
||||
(ngModelChange)='config.save()'
|
||||
)
|
||||
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Custom CSS
|
||||
|
||||
textarea.form-control.mb-5(
|
||||
[(ngModel)]='config.store.appearance.css',
|
||||
(ngModelChange)='saveConfiguration()',
|
||||
)
|
||||
|
@@ -2,3 +2,8 @@ color-scheme-preview {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Observable } from 'rxjs'
|
||||
import { debounce } from 'utils-decorators/dist/cjs'
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { exec } from 'mz/child_process'
|
||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||
@@ -23,11 +24,7 @@ export class AppearanceSettingsTabComponent {
|
||||
async ngOnInit () {
|
||||
if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
|
||||
const fonts = await new Promise<any[]>((resolve) => fontManager.findFonts({ monospace: true }, resolve))
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
this.fonts = fonts.map(x => `${x.family} ${x.style}`.trim())
|
||||
} else {
|
||||
this.fonts = fonts.map(x => x.family.trim())
|
||||
}
|
||||
this.fonts = fonts.map(x => x.family.trim())
|
||||
this.fonts.sort()
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Linux) {
|
||||
@@ -54,4 +51,12 @@ export class AppearanceSettingsTabComponent {
|
||||
getPreviewFontFamily () {
|
||||
return getCSSFontFamily(this.config.store)
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
saveConfiguration (requireRestart?: boolean) {
|
||||
this.config.save()
|
||||
if (requireRestart) {
|
||||
this.config.requestRestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
input.search-input.form-control(
|
||||
type='search',
|
||||
type='text',
|
||||
[(ngModel)]='query',
|
||||
(ngModelChange)='onQueryChange()',
|
||||
[class.text-danger]='notFound',
|
||||
@@ -11,14 +11,14 @@ input.search-input.form-control(
|
||||
|
||||
button.btn.btn-link(
|
||||
(click)='findPrevious()',
|
||||
ngbTooltip='Next',
|
||||
ngbTooltip='Search up',
|
||||
placement='bottom'
|
||||
)
|
||||
i.fa.fa-fw.fa-arrow-up
|
||||
|
||||
button.btn.btn-link(
|
||||
(click)='findNext()',
|
||||
ngbTooltip='Next',
|
||||
ngbTooltip='Search down',
|
||||
placement='bottom'
|
||||
)
|
||||
i.fa.fa-fw.fa-arrow-down
|
||||
@@ -48,4 +48,7 @@ button.btn.btn-link(
|
||||
)
|
||||
i.fa.fa-fw.fa-text-width
|
||||
|
||||
button.close.text-light.pl-3.pr-2((click)='close.emit()') ×
|
||||
.mr-2
|
||||
|
||||
button.btn.btn-link((click)='close.emit()')
|
||||
i.fa.fa-fw.fa-times
|
||||
|
@@ -1,5 +1,18 @@
|
||||
h3.mb-3 Terminal
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Frontend
|
||||
.description Switches terminal frontend implementation (experimental)
|
||||
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.terminal.frontend',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(value='hterm') hterm
|
||||
option(value='xterm') xterm
|
||||
option(value='xterm-webgl') xterm (WebGL)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Terminal bell
|
||||
@@ -116,7 +129,7 @@ h3.mb-3 Terminal
|
||||
[(ngModel)]='config.store.terminal.scrollOnInput',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Use Alt key as the Meta key
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { Component, Input, Injector } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
|
||||
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
|
||||
import { SessionOptions } from '../api/interfaces'
|
||||
@@ -15,7 +13,7 @@ import { Session } from '../services/sessions.service'
|
||||
})
|
||||
export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
@Input() sessionOptions: SessionOptions
|
||||
private homeEndSubscription: Subscription
|
||||
session: Session|null = null
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
@@ -30,7 +28,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
const isConPTY = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY
|
||||
|
||||
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
@@ -44,13 +42,15 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
}
|
||||
})
|
||||
|
||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||
this.initializeSession(this.size.columns, this.size.rows)
|
||||
})
|
||||
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
protected onFrontendReady (): void {
|
||||
this.initializeSession(this.size.columns, this.size.rows)
|
||||
this.savedStateIsLive = this.sessionOptions.restoreFromPTYID === this.session?.getPTYID()
|
||||
super.onFrontendReady()
|
||||
}
|
||||
|
||||
initializeSession (columns: number, rows: number): void {
|
||||
this.sessions.addSession(
|
||||
this.session!,
|
||||
@@ -61,6 +61,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
)
|
||||
|
||||
this.attachSessionHandlers(true)
|
||||
this.recoveryStateChangedHint.next()
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
@@ -70,6 +71,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
sessionOptions: {
|
||||
...this.sessionOptions,
|
||||
cwd: cwd ?? this.sessionOptions.cwd,
|
||||
restoreFromPTYID: this.session?.getPTYID(),
|
||||
},
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
@@ -102,7 +104,6 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy (): void {
|
||||
this.homeEndSubscription.unsubscribe()
|
||||
super.ngOnDestroy()
|
||||
this.session?.destroy()
|
||||
}
|
||||
|
@@ -75,6 +75,7 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
caseSensitive: false,
|
||||
},
|
||||
detectProgress: true,
|
||||
scrollbackLines: 25000,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
clear: [
|
||||
'⌘-K',
|
||||
],
|
||||
'select-all': ['⌘-A'],
|
||||
'zoom-in': [
|
||||
'⌘-=',
|
||||
'⌘-Shift-=',
|
||||
@@ -141,6 +143,7 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
paste: [
|
||||
'Ctrl-Shift-V',
|
||||
],
|
||||
'select-all': ['Ctrl-Shift-A'],
|
||||
clear: [],
|
||||
'zoom-in': [
|
||||
'Ctrl-=',
|
||||
@@ -184,6 +187,7 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
paste: [
|
||||
'Ctrl-Shift-V',
|
||||
],
|
||||
'select-all': ['Ctrl-Shift-A'],
|
||||
clear: [],
|
||||
'zoom-in': [
|
||||
'Ctrl-=',
|
||||
|