mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-22 19:38:00 +00:00
Compare commits
291 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d0ddd82906 | ||
![]() |
8a5846ff81 | ||
![]() |
298209b03a | ||
![]() |
dc03a7f135 | ||
![]() |
2d357d0ed2 | ||
![]() |
58d2590495 | ||
![]() |
0c892225f3 | ||
![]() |
851d92a140 | ||
![]() |
665ce2f022 | ||
![]() |
5ea1ff1ea5 | ||
![]() |
5ed92342ea | ||
![]() |
d9843c4ff0 | ||
![]() |
448371dffa | ||
![]() |
425c288aa0 | ||
![]() |
09b118987e | ||
![]() |
70dbb6bfcc | ||
![]() |
4b55f9cd97 | ||
![]() |
fbb6614553 | ||
![]() |
8d849b40a2 | ||
![]() |
5e678c15f3 | ||
![]() |
6642be3050 | ||
![]() |
9998fa97f7 | ||
![]() |
2b3113b67d | ||
![]() |
f933c29e5e | ||
![]() |
a908d737a4 | ||
![]() |
469b4e4f44 | ||
![]() |
13b31d16e8 | ||
![]() |
afa2eb3a6e | ||
![]() |
01eb26e5c1 | ||
![]() |
b967a1ecfa | ||
![]() |
dfe91e98c3 | ||
![]() |
6706aa67eb | ||
![]() |
15fe4f34f9 | ||
![]() |
9b7c446fd7 | ||
![]() |
9225239433 | ||
![]() |
e764b22698 | ||
![]() |
a4d88e4631 | ||
![]() |
20da700ad4 | ||
![]() |
e4a4937023 | ||
![]() |
822b60b454 | ||
![]() |
72a024c713 | ||
![]() |
6132881b8a | ||
![]() |
1ac22ec563 | ||
![]() |
ca68905b05 | ||
![]() |
2470f5f941 | ||
![]() |
fd1ea4fc49 | ||
![]() |
3f8b933d05 | ||
![]() |
3b13f0d73f | ||
![]() |
a261efbc01 | ||
![]() |
04d4474648 | ||
![]() |
4a50c84cfe | ||
![]() |
9ca091e592 | ||
![]() |
fa56f30f63 | ||
![]() |
9c19307181 | ||
![]() |
bfd34df41e | ||
![]() |
26ee36458d | ||
![]() |
e99b83dfdc | ||
![]() |
ceb75323fe | ||
![]() |
bfb6417865 | ||
![]() |
498564be9a | ||
![]() |
6c38aa4008 | ||
![]() |
32aaa3d0ff | ||
![]() |
7ce5d647da | ||
![]() |
bf0be7fa0e | ||
![]() |
a10eb5ff90 | ||
![]() |
c6a27d8893 | ||
![]() |
7ee1fb4b76 | ||
![]() |
e0f9f558f1 | ||
![]() |
d511bb9fc0 | ||
![]() |
15f99c8ae8 | ||
![]() |
1027fbfb60 | ||
![]() |
d89abde860 | ||
![]() |
1d76f6b056 | ||
![]() |
f09fb735a7 | ||
![]() |
f6d08af986 | ||
![]() |
7ddb67f28e | ||
![]() |
ff2c2031b5 | ||
![]() |
1ab528b05c | ||
![]() |
9ae92ef88c | ||
![]() |
f4f52321f5 | ||
![]() |
286b6cfdde | ||
![]() |
b97149aa73 | ||
![]() |
2094c44992 | ||
![]() |
a8409a0aec | ||
![]() |
5c2ac72bac | ||
![]() |
b58d6e1bfd | ||
![]() |
be09e7829f | ||
![]() |
a9320a33c9 | ||
![]() |
0e881e47e2 | ||
![]() |
a3a5da8550 | ||
![]() |
178a2e34c6 | ||
![]() |
b39276feca | ||
![]() |
3b7f4be643 | ||
![]() |
2c40f0dddc | ||
![]() |
8f361f841c | ||
![]() |
0e0d59f2c5 | ||
![]() |
ca9b8307c2 | ||
![]() |
d837a3d7fd | ||
![]() |
555e1e733d | ||
![]() |
2e546fb72e | ||
![]() |
a2d4b76070 | ||
![]() |
34edee2f37 | ||
![]() |
f17b7fb8ce | ||
![]() |
cbe5ea7fdd | ||
![]() |
c00405afff | ||
![]() |
555db7bef9 | ||
![]() |
94e9e7d439 | ||
![]() |
c4850f2172 | ||
![]() |
2d33a0030d | ||
![]() |
1f576edf85 | ||
![]() |
6d139434d7 | ||
![]() |
e9ba07c7a3 | ||
![]() |
4f083098c3 | ||
![]() |
03369da32c | ||
![]() |
edb2cf2b7b | ||
![]() |
91a1ae1cbe | ||
![]() |
4a9f17ace9 | ||
![]() |
d5babde1fa | ||
![]() |
902c1fceb1 | ||
![]() |
1d340e4666 | ||
![]() |
b922fe7776 | ||
![]() |
de3829a94c | ||
![]() |
964e0ad2bc | ||
![]() |
742affb88b | ||
![]() |
e9999ee883 | ||
![]() |
1c8288bfe1 | ||
![]() |
37044fbb01 | ||
![]() |
5507171fee | ||
![]() |
472b421484 | ||
![]() |
b076541962 | ||
![]() |
8201e0b9ef | ||
![]() |
6293a43571 | ||
![]() |
b61bc943ec | ||
![]() |
be668403c5 | ||
![]() |
90a173d8b7 | ||
![]() |
bbcc84e9b0 | ||
![]() |
b0a8832499 | ||
![]() |
8cd1c4a9af | ||
![]() |
1ada4338b7 | ||
![]() |
4a701fa7b4 | ||
![]() |
b718448904 | ||
![]() |
7770cf2573 | ||
![]() |
49755f855f | ||
![]() |
daa1b4572e | ||
![]() |
d48e22a9d2 | ||
![]() |
fc82010729 | ||
![]() |
4a8f3fbd7f | ||
![]() |
4c435672a5 | ||
![]() |
0311754ce0 | ||
![]() |
ff5da104c1 | ||
![]() |
b01b902829 | ||
![]() |
595707eed4 | ||
![]() |
441ee4fb6e | ||
![]() |
51827d6750 | ||
![]() |
9807bbe32a | ||
![]() |
181b55890d | ||
![]() |
c8c5f1a0fd | ||
![]() |
222c6a9f3c | ||
![]() |
0400c8fe63 | ||
![]() |
099d9b06d6 | ||
![]() |
ad26b39cca | ||
![]() |
f465c359ef | ||
![]() |
67aead225c | ||
![]() |
280c421ae4 | ||
![]() |
b6fc43faa2 | ||
![]() |
b5a985b8a3 | ||
![]() |
2f7dcf3339 | ||
![]() |
7e38f11c06 | ||
![]() |
86cd560089 | ||
![]() |
c0c4580461 | ||
![]() |
5cb65dfd84 | ||
![]() |
2b5f623b50 | ||
![]() |
a8d5cf469e | ||
![]() |
d261b89803 | ||
![]() |
21cb452d62 | ||
![]() |
b07a2113d2 | ||
![]() |
f545b3eacf | ||
![]() |
87fe8deaa8 | ||
![]() |
1068450ddd | ||
![]() |
b355fff0f8 | ||
![]() |
f80b0eb65b | ||
![]() |
285691326f | ||
![]() |
3ecffbfda6 | ||
![]() |
3d89a15d18 | ||
![]() |
491d4c3b3a | ||
![]() |
995f329835 | ||
![]() |
28f2ea595d | ||
![]() |
42b7c573ea | ||
![]() |
c40294628a | ||
![]() |
c11a10144e | ||
![]() |
7b6cdb274c | ||
![]() |
a3c74ecdba | ||
![]() |
94d91f8182 | ||
![]() |
e4f32c9ade | ||
![]() |
65fd7b05b1 | ||
![]() |
2150fab55b | ||
![]() |
644cb76fd3 | ||
![]() |
4106d97f6b | ||
![]() |
98103fd139 | ||
![]() |
9453e8ba7b | ||
![]() |
2f78575cd7 | ||
![]() |
500acee064 | ||
![]() |
42eb5f6b78 | ||
![]() |
ef19b92e85 | ||
![]() |
f263f954d4 | ||
![]() |
2ce0f03282 | ||
![]() |
150999d3a3 | ||
![]() |
8cc76555d2 | ||
![]() |
f0f8f06890 | ||
![]() |
176a55c91d | ||
![]() |
fc6dfc50dd | ||
![]() |
34d020f66a | ||
![]() |
fa0ef69c46 | ||
![]() |
e5c1e421f7 | ||
![]() |
f3994f1bd9 | ||
![]() |
6956ef9e0f | ||
![]() |
a080129882 | ||
![]() |
ef9bfe6120 | ||
![]() |
37d69e858f | ||
![]() |
ee594f5bcd | ||
![]() |
adf022de2c | ||
![]() |
c5a9b890c4 | ||
![]() |
2d1a96a12b | ||
![]() |
5289981485 | ||
![]() |
a89047b205 | ||
![]() |
a7b4496d22 | ||
![]() |
09cd9d0e18 | ||
![]() |
fb2a4d268d | ||
![]() |
9b61615701 | ||
![]() |
077d2421e1 | ||
![]() |
202ba18a8c | ||
![]() |
63f33f8f4b | ||
![]() |
3bd89a0194 | ||
![]() |
604bc28c9a | ||
![]() |
f81f5d122a | ||
![]() |
3633be750e | ||
![]() |
404fd72ea9 | ||
![]() |
402b76bcc9 | ||
![]() |
b6c97ffa49 | ||
![]() |
20aa1d814f | ||
![]() |
786daaac32 | ||
![]() |
0360ad2dd0 | ||
![]() |
0a451c5876 | ||
![]() |
5a9625424c | ||
![]() |
62c1f6463b | ||
![]() |
9fe82f2c0a | ||
![]() |
09838197a2 | ||
![]() |
27114797a2 | ||
![]() |
4dc77d11cf | ||
![]() |
245698b67d | ||
![]() |
017fabaf6f | ||
![]() |
6c11189b3e | ||
![]() |
dd70f5f5d8 | ||
![]() |
47277ac5aa | ||
![]() |
b69cbbcdd1 | ||
![]() |
efba980a1d | ||
![]() |
f31da67508 | ||
![]() |
2ba76cc0b9 | ||
![]() |
62d14ac0cb | ||
![]() |
c1b4ffd248 | ||
![]() |
87cacdb568 | ||
![]() |
2a11bc4fcc | ||
![]() |
f716baa7d4 | ||
![]() |
5b60daf366 | ||
![]() |
11f9f4e824 | ||
![]() |
0daf48f699 | ||
![]() |
8fb0ea4d75 | ||
![]() |
d9948cf6e2 | ||
![]() |
54b618cffc | ||
![]() |
690dde628e | ||
![]() |
3dfbcf9d41 | ||
![]() |
d91ba71ec0 | ||
![]() |
99698913a8 | ||
![]() |
0dbb16d859 | ||
![]() |
0f8cff2d5b | ||
![]() |
471f9effcf | ||
![]() |
04faf1a04a | ||
![]() |
656f5c2561 | ||
![]() |
99bc2c1c65 | ||
![]() |
87837bf66b | ||
![]() |
c8735243f3 | ||
![]() |
b197a16e5c | ||
![]() |
1a361e67b3 | ||
![]() |
fc471b2c16 | ||
![]() |
ae17faa7e5 | ||
![]() |
5fb70f1812 | ||
![]() |
03fc68bb6d | ||
![]() |
bb9c80623d | ||
![]() |
4dd0a5951f | ||
![]() |
b010791767 | ||
![]() |
ef61a141a6 |
@@ -225,6 +225,24 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "LeSeulArtichaut",
|
||||||
|
"name": "LeSeulArtichaut",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/38361244?v=4",
|
||||||
|
"profile": "https://github.com/LeSeulArtichaut",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "CyrilTaylor",
|
||||||
|
"name": "Cyril Taylor",
|
||||||
|
"avatar_url": "https://avatars0.githubusercontent.com/u/12631466?v=4",
|
||||||
|
"profile": "https://github.com/CyrilTaylor",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
@@ -232,5 +250,6 @@
|
|||||||
"projectOwner": "Eugeny",
|
"projectOwner": "Eugeny",
|
||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"commitConvention": "none"
|
"commitConvention": "none",
|
||||||
|
"skipCi": true
|
||||||
}
|
}
|
||||||
|
@@ -79,6 +79,7 @@ rules:
|
|||||||
args: after-used
|
args: after-used
|
||||||
argsIgnorePattern: ^_
|
argsIgnorePattern: ^_
|
||||||
no-undef: error
|
no-undef: error
|
||||||
|
no-var: error
|
||||||
object-curly-spacing:
|
object-curly-spacing:
|
||||||
- error
|
- error
|
||||||
- always
|
- always
|
||||||
@@ -97,3 +98,4 @@ rules:
|
|||||||
'@typescript-eslint/no-untyped-public-signature': off # bugs out on constructors
|
'@typescript-eslint/no-untyped-public-signature': off # bugs out on constructors
|
||||||
'@typescript-eslint/restrict-template-expressions': off
|
'@typescript-eslint/restrict-template-expressions': off
|
||||||
'@typescript-eslint/no-dynamic-delete': off
|
'@typescript-eslint/no-dynamic-delete': off
|
||||||
|
'@typescript-eslint/prefer-nullish-coalescing': off
|
||||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: scripts/build-macos.js
|
run: scripts/build-macos.js
|
||||||
if: github.repository == 'Eugeny/terminus' && github.event_name == 'push'
|
if: github.repository == 'Eugeny/terminus' && github.event_name == 'push'
|
||||||
env:
|
env:
|
||||||
DEBUG: electron-builder,electron-builder:*
|
#DEBUG: electron-builder,electron-builder:*
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
mkdir artifact-setup
|
mkdir artifact-setup
|
||||||
mv dist/*-setup.exe artifact-setup/
|
mv dist/*-setup.exe artifact-setup/
|
||||||
mkdir artifact-portable
|
mkdir artifact-portable
|
||||||
mv dist/*-portable.exe artifact-portable/
|
mv dist/*-portable.zip artifact-portable/
|
||||||
|
|
||||||
- uses: actions/upload-artifact@master
|
- uses: actions/upload-artifact@master
|
||||||
name: Upload installer
|
name: Upload installer
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@ yarn-error.log
|
|||||||
docs/api
|
docs/api
|
||||||
.travis.ssh.key
|
.travis.ssh.key
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|
||||||
|
.electron-symbols
|
||||||
|
sentry.properties
|
||||||
|
sentry-symbols.js
|
||||||
|
63
README.md
63
README.md
@@ -13,12 +13,13 @@
|
|||||||
|
|
||||||
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
|
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
|
||||||
|
|
||||||
|
* Integrated SSH client and connection manager
|
||||||
* Theming and color schemes
|
* Theming and color schemes
|
||||||
* Fully configurable shortcuts
|
* Fully configurable shortcuts
|
||||||
* Split panes
|
* Split panes
|
||||||
* Remembers your tabs
|
* Remembers your tabs
|
||||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
||||||
* Integrated SSH client and connection manager
|
* Direct file transfer from/to SSH sessions via Zmodem
|
||||||
* Full Unicode support including double-width characters
|
* Full Unicode support including double-width characters
|
||||||
* Doesn't choke on fast-flowing outputs
|
* Doesn't choke on fast-flowing outputs
|
||||||
* Proper shell experience on Windows including tab completion (via Clink)
|
* Proper shell experience on Windows including tab completion (via Clink)
|
||||||
@@ -34,12 +35,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Portable
|
||||||
|
|
||||||
|
For portable in windows, user can create folder `data` at the same directory as `Terminal.exe` to save the settings.
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
|
|
||||||
Plugins and themes can be installed directly from the Settings view inside Terminus.
|
Plugins and themes can be installed directly from the Settings view inside Terminus.
|
||||||
|
|
||||||
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
|
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
|
||||||
* [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
|
|
||||||
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
||||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||||
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
|
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
|
||||||
@@ -66,42 +70,47 @@ See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) and
|
|||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="http://www.russellmyers.com"><img src="https://avatars2.githubusercontent.com/u/184085?v=4" width="100px;" alt="Russell Myers"/><br /><sub><b>Russell Myers</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mezner" title="Code">💻</a></td>
|
<td align="center"><a href="http://www.russellmyers.com"><img src="https://avatars2.githubusercontent.com/u/184085?v=4" width="100px;" alt=""/><br /><sub><b>Russell Myers</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mezner" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://www.morwire.com"><img src="https://avatars1.githubusercontent.com/u/3991658?v=4" width="100px;" alt="Austin Warren"/><br /><sub><b>Austin Warren</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ehwarren" title="Code">💻</a></td>
|
<td align="center"><a href="http://www.morwire.com"><img src="https://avatars1.githubusercontent.com/u/3991658?v=4" width="100px;" alt=""/><br /><sub><b>Austin Warren</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ehwarren" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/Drachenkaetzchen"><img src="https://avatars1.githubusercontent.com/u/162974?v=4" width="100px;" alt="Felicia Hummel"/><br /><sub><b>Felicia Hummel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Drachenkaetzchen" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/Drachenkaetzchen"><img src="https://avatars1.githubusercontent.com/u/162974?v=4" width="100px;" alt=""/><br /><sub><b>Felicia Hummel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Drachenkaetzchen" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/mikemaccana"><img src="https://avatars2.githubusercontent.com/u/172594?v=4" width="100px;" alt="Mike MacCana"/><br /><sub><b>Mike MacCana</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mikemaccana" title="Tests">⚠️</a> <a href="#design-mikemaccana" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/mikemaccana"><img src="https://avatars2.githubusercontent.com/u/172594?v=4" width="100px;" alt=""/><br /><sub><b>Mike MacCana</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mikemaccana" title="Tests">⚠️</a> <a href="#design-mikemaccana" title="Design">🎨</a></td>
|
||||||
<td align="center"><a href="https://github.com/yxuko"><img src="https://avatars1.githubusercontent.com/u/1786317?v=4" width="100px;" alt="Yacine Kanzari"/><br /><sub><b>Yacine Kanzari</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yxuko" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/yxuko"><img src="https://avatars1.githubusercontent.com/u/1786317?v=4" width="100px;" alt=""/><br /><sub><b>Yacine Kanzari</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yxuko" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/BBJip"><img src="https://avatars2.githubusercontent.com/u/32908927?v=4" width="100px;" alt="BBJip"/><br /><sub><b>BBJip</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=BBJip" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/BBJip"><img src="https://avatars2.githubusercontent.com/u/32908927?v=4" width="100px;" alt=""/><br /><sub><b>BBJip</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=BBJip" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/Futagirl"><img src="https://avatars2.githubusercontent.com/u/33533958?v=4" width="100px;" alt="Futagirl"/><br /><sub><b>Futagirl</b></sub></a><br /><a href="#design-Futagirl" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/Futagirl"><img src="https://avatars2.githubusercontent.com/u/33533958?v=4" width="100px;" alt=""/><br /><sub><b>Futagirl</b></sub></a><br /><a href="#design-Futagirl" title="Design">🎨</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="https://www.levrik.io"><img src="https://avatars3.githubusercontent.com/u/9491603?v=4" width="100px;" alt="Levin Rickert"/><br /><sub><b>Levin Rickert</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=levrik" title="Code">💻</a></td>
|
<td align="center"><a href="https://www.levrik.io"><img src="https://avatars3.githubusercontent.com/u/9491603?v=4" width="100px;" alt=""/><br /><sub><b>Levin Rickert</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=levrik" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://kwonoj.github.io"><img src="https://avatars2.githubusercontent.com/u/1210596?v=4" width="100px;" alt="OJ Kwon"/><br /><sub><b>OJ Kwon</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=kwonoj" title="Code">💻</a></td>
|
<td align="center"><a href="https://kwonoj.github.io"><img src="https://avatars2.githubusercontent.com/u/1210596?v=4" width="100px;" alt=""/><br /><sub><b>OJ Kwon</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=kwonoj" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/Domain"><img src="https://avatars2.githubusercontent.com/u/903197?v=4" width="100px;" alt="domain"/><br /><sub><b>domain</b></sub></a><br /><a href="#plugin-Domain" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Domain" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/Domain"><img src="https://avatars2.githubusercontent.com/u/903197?v=4" width="100px;" alt=""/><br /><sub><b>domain</b></sub></a><br /><a href="#plugin-Domain" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Domain" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://www.jbrumond.me"><img src="https://avatars1.githubusercontent.com/u/195127?v=4" width="100px;" alt="James Brumond"/><br /><sub><b>James Brumond</b></sub></a><br /><a href="#plugin-kbjr" title="Plugin/utility libraries">🔌</a></td>
|
<td align="center"><a href="http://www.jbrumond.me"><img src="https://avatars1.githubusercontent.com/u/195127?v=4" width="100px;" alt=""/><br /><sub><b>James Brumond</b></sub></a><br /><a href="#plugin-kbjr" title="Plugin/utility libraries">🔌</a></td>
|
||||||
<td align="center"><a href="http://www.growingwiththeweb.com"><img src="https://avatars0.githubusercontent.com/u/2193314?v=4" width="100px;" alt="Daniel Imms"/><br /><sub><b>Daniel Imms</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Code">💻</a> <a href="#plugin-Tyriar" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Tests">⚠️</a></td>
|
<td align="center"><a href="http://www.growingwiththeweb.com"><img src="https://avatars0.githubusercontent.com/u/2193314?v=4" width="100px;" alt=""/><br /><sub><b>Daniel Imms</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Code">💻</a> <a href="#plugin-Tyriar" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Tests">⚠️</a></td>
|
||||||
<td align="center"><a href="https://github.com/baflo"><img src="https://avatars2.githubusercontent.com/u/834350?v=4" width="100px;" alt="Florian Bachmann"/><br /><sub><b>Florian Bachmann</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=baflo" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/baflo"><img src="https://avatars2.githubusercontent.com/u/834350?v=4" width="100px;" alt=""/><br /><sub><b>Florian Bachmann</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=baflo" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://michael-kuehnel.de"><img src="https://avatars2.githubusercontent.com/u/441011?v=4" width="100px;" alt="Michael Kühnel"/><br /><sub><b>Michael Kühnel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mischah" title="Code">💻</a> <a href="#design-mischah" title="Design">🎨</a></td>
|
<td align="center"><a href="http://michael-kuehnel.de"><img src="https://avatars2.githubusercontent.com/u/441011?v=4" width="100px;" alt=""/><br /><sub><b>Michael Kühnel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mischah" title="Code">💻</a> <a href="#design-mischah" title="Design">🎨</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="https://github.com/NieLeben"><img src="https://avatars3.githubusercontent.com/u/47182955?v=4" width="100px;" alt="Tilmann Meyer"/><br /><sub><b>Tilmann Meyer</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=NieLeben" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/NieLeben"><img src="https://avatars3.githubusercontent.com/u/47182955?v=4" width="100px;" alt=""/><br /><sub><b>Tilmann Meyer</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=NieLeben" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://www.jubeat.net"><img src="https://avatars3.githubusercontent.com/u/11289158?v=4" width="100px;" alt="PM Extra"/><br /><sub><b>PM Extra</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/issues?q=author%3APMExtra" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="http://www.jubeat.net"><img src="https://avatars3.githubusercontent.com/u/11289158?v=4" width="100px;" alt=""/><br /><sub><b>PM Extra</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/issues?q=author%3APMExtra" title="Bug reports">🐛</a></td>
|
||||||
<td align="center"><a href="https://jjuhas.keybase.pub//"><img src="https://avatars1.githubusercontent.com/u/6438760?v=4" width="100px;" alt="Jonathan"/><br /><sub><b>Jonathan</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=IgnusG" title="Code">💻</a></td>
|
<td align="center"><a href="https://jjuhas.keybase.pub//"><img src="https://avatars1.githubusercontent.com/u/6438760?v=4" width="100px;" alt=""/><br /><sub><b>Jonathan</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=IgnusG" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://hans-koch.me"><img src="https://avatars0.githubusercontent.com/u/1093709?v=4" width="100px;" alt="Hans Koch"/><br /><sub><b>Hans Koch</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hammster" title="Code">💻</a></td>
|
<td align="center"><a href="https://hans-koch.me"><img src="https://avatars0.githubusercontent.com/u/1093709?v=4" width="100px;" alt=""/><br /><sub><b>Hans Koch</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hammster" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://thepuzzlemaker.info"><img src="https://avatars3.githubusercontent.com/u/12666617?v=4" width="100px;" alt="Dak Smyth"/><br /><sub><b>Dak Smyth</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ThePuzzlemaker" title="Code">💻</a></td>
|
<td align="center"><a href="http://thepuzzlemaker.info"><img src="https://avatars3.githubusercontent.com/u/12666617?v=4" width="100px;" alt=""/><br /><sub><b>Dak Smyth</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ThePuzzlemaker" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://yfwz100.github.io"><img src="https://avatars2.githubusercontent.com/u/983211?v=4" width="100px;" alt="Wang Zhi"/><br /><sub><b>Wang Zhi</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yfwz100" title="Code">💻</a></td>
|
<td align="center"><a href="http://yfwz100.github.io"><img src="https://avatars2.githubusercontent.com/u/983211?v=4" width="100px;" alt=""/><br /><sub><b>Wang Zhi</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yfwz100" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/jack1142"><img src="https://avatars0.githubusercontent.com/u/6032823?v=4" width="100px;" alt="jack1142"/><br /><sub><b>jack1142</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=jack1142" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/jack1142"><img src="https://avatars0.githubusercontent.com/u/6032823?v=4" width="100px;" alt=""/><br /><sub><b>jack1142</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=jack1142" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="https://github.com/hdougie"><img src="https://avatars1.githubusercontent.com/u/450799?v=4" width="100px;" alt="Howie Douglas"/><br /><sub><b>Howie Douglas</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hdougie" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/hdougie"><img src="https://avatars1.githubusercontent.com/u/450799?v=4" width="100px;" alt=""/><br /><sub><b>Howie Douglas</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hdougie" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://chriskaczor.com"><img src="https://avatars2.githubusercontent.com/u/180906?v=4" width="100px;" alt="Chris Kaczor"/><br /><sub><b>Chris Kaczor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ckaczor" title="Code">💻</a></td>
|
<td align="center"><a href="https://chriskaczor.com"><img src="https://avatars2.githubusercontent.com/u/180906?v=4" width="100px;" alt=""/><br /><sub><b>Chris Kaczor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ckaczor" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://www.boxmein.net"><img src="https://avatars1.githubusercontent.com/u/358714?v=4" width="100px;" alt="Johannes Kadak"/><br /><sub><b>Johannes Kadak</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=boxmein" title="Code">💻</a></td>
|
<td align="center"><a href="https://www.boxmein.net"><img src="https://avatars1.githubusercontent.com/u/358714?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Kadak</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=boxmein" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/LeSeulArtichaut"><img src="https://avatars1.githubusercontent.com/u/38361244?v=4" width="100px;" alt=""/><br /><sub><b>LeSeulArtichaut</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=LeSeulArtichaut" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/CyrilTaylor"><img src="https://avatars0.githubusercontent.com/u/12631466?v=4" width="100px;" alt=""/><br /><sub><b>Cyril Taylor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=CyrilTaylor" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-enable -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
||||||
|
// eslint-disable-next-line no-duplicate-imports
|
||||||
import * as electron from 'electron'
|
import * as electron from 'electron'
|
||||||
import { loadConfig } from './config'
|
import { loadConfig } from './config'
|
||||||
import { Window, WindowOptions } from './window'
|
import { Window, WindowOptions } from './window'
|
||||||
@@ -13,16 +14,19 @@ export class Application {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const configData = loadConfig()
|
const configData = loadConfig()
|
||||||
if (process.platform === 'linux' && ((configData.appearance || {}).opacity || 1) !== 1) {
|
if (process.platform === 'linux') {
|
||||||
app.commandLine.appendSwitch('enable-transparent-visuals')
|
app.commandLine.appendSwitch('no-sandbox')
|
||||||
app.disableHardwareAcceleration()
|
if (((configData.appearance || {}).opacity || 1) !== 1) {
|
||||||
|
app.commandLine.appendSwitch('enable-transparent-visuals')
|
||||||
|
app.disableHardwareAcceleration()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-http-cache')
|
app.commandLine.appendSwitch('disable-http-cache')
|
||||||
app.commandLine.appendSwitch('lang', 'EN')
|
app.commandLine.appendSwitch('lang', 'EN')
|
||||||
|
app.allowRendererProcessReuse = false
|
||||||
|
|
||||||
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
|
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
|
||||||
console.log('Setting Electron flag:', flag.join('='))
|
|
||||||
app.commandLine.appendSwitch(flag[0], flag[1])
|
app.commandLine.appendSwitch(flag[0], flag[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,7 @@ export class Application {
|
|||||||
this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
|
this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tray.on('click', () => setTimeout(() => this.focus()));
|
this.tray.on('click', () => setTimeout(() => this.focus()))
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([{
|
const contextMenu = Menu.buildFromTemplate([{
|
||||||
label: 'Show',
|
label: 'Show',
|
||||||
@@ -183,7 +187,7 @@ export class Application {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||||
|
@@ -20,25 +20,25 @@ export function parseArgs (argv, cwd) {
|
|||||||
return yargs.option('escape', {
|
return yargs.option('escape', {
|
||||||
alias: 'e',
|
alias: 'e',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
describe: 'Perform shell escaping'
|
describe: 'Perform shell escaping',
|
||||||
}).positional('text', {
|
}).positional('text', {
|
||||||
type: 'string'
|
type: 'string',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.version('version', '', app.getVersion())
|
.version('version', '', app.getVersion())
|
||||||
.option('debug', {
|
.option('debug', {
|
||||||
alias: 'd',
|
alias: 'd',
|
||||||
describe: 'Show DevTools on start',
|
describe: 'Show DevTools on start',
|
||||||
type: 'boolean'
|
type: 'boolean',
|
||||||
})
|
})
|
||||||
.option('hidden', {
|
.option('hidden', {
|
||||||
describe: 'Start minimized',
|
describe: 'Start minimized',
|
||||||
type: 'boolean'
|
type: 'boolean',
|
||||||
})
|
})
|
||||||
.option('version', {
|
.option('version', {
|
||||||
alias: 'v',
|
alias: 'v',
|
||||||
describe: 'Show version and exit',
|
describe: 'Show version and exit',
|
||||||
type: 'boolean'
|
type: 'boolean',
|
||||||
})
|
})
|
||||||
.help('help')
|
.help('help')
|
||||||
.parse(argv.slice(1))
|
.parse(argv.slice(1))
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import './portable'
|
||||||
|
import './sentry'
|
||||||
import './lru'
|
import './lru'
|
||||||
import { app, ipcMain, Menu } from 'electron'
|
import { app, ipcMain, Menu } from 'electron'
|
||||||
import { parseArgs } from './cli'
|
import { parseArgs } from './cli'
|
||||||
@@ -46,7 +48,7 @@ if (argv.d) {
|
|||||||
electronDebug({
|
electronDebug({
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
showDevTools: true,
|
showDevTools: true,
|
||||||
devToolsMode: 'undocked'
|
devToolsMode: 'undocked',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +59,8 @@ app.on('ready', () => {
|
|||||||
label: 'New window',
|
label: 'New window',
|
||||||
click () {
|
click () {
|
||||||
this.app.newWindow()
|
this.app.newWindow()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
application.init()
|
application.init()
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
let lru = require('lru-cache')({ max: 256, maxAge: 250 })
|
import * as createLRU from 'lru-cache'
|
||||||
|
import * as fs from 'fs'
|
||||||
let fs = require('fs')
|
const lru = createLRU({ max: 256, maxAge: 250 })
|
||||||
let origLstat = fs.realpathSync.bind(fs)
|
const origLstat = fs.realpathSync.bind(fs)
|
||||||
|
|
||||||
// NB: The biggest offender of thrashing realpathSync is the node module system
|
// NB: The biggest offender of thrashing realpathSync is the node module system
|
||||||
// itself, which we can't get into via any sane means.
|
// itself, which we can't get into via any sane means.
|
||||||
require('fs').realpathSync = function (p) {
|
require('fs').realpathSync = function (p) {
|
||||||
let r = lru.get(p)
|
let r = lru.get(p)
|
||||||
if (r) return r
|
if (r) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
r = origLstat(p)
|
r = origLstat(p)
|
||||||
lru.set(p, r)
|
lru.set(p, r)
|
||||||
|
24
app/lib/portable.ts
Executable file
24
app/lib/portable.ts
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
let appPath: string | null = null
|
||||||
|
try {
|
||||||
|
appPath = path.dirname(require('electron').app.getPath('exe'))
|
||||||
|
} catch {
|
||||||
|
appPath = path.dirname(require('electron').remote.app.getPath('exe'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != appPath) {
|
||||||
|
if(fs.existsSync(path.join(appPath, 'terminus-data'))) {
|
||||||
|
fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data'))
|
||||||
|
}
|
||||||
|
const portableData = path.join(appPath, 'data')
|
||||||
|
if (fs.existsSync(portableData)) {
|
||||||
|
console.log('reset user data to ' + portableData)
|
||||||
|
try {
|
||||||
|
require('electron').app.setPath('userData', portableData)
|
||||||
|
} catch {
|
||||||
|
require('electron').remote.app.setPath('userData', portableData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
app/lib/sentry.ts
Executable file
21
app/lib/sentry.ts
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
const { init } = 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
|
||||||
|
try {
|
||||||
|
release = require('electron').app.getVersion()
|
||||||
|
} catch {
|
||||||
|
release = require('electron').remote.app.getVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDev) {
|
||||||
|
init({
|
||||||
|
dsn: SENTRY_DSN,
|
||||||
|
release,
|
||||||
|
integrations (integrations) {
|
||||||
|
return integrations.filter(integration => integration.name !== 'Breadcrumbs')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { debounceTime } from 'rxjs/operators'
|
|||||||
import { BrowserWindow, app, ipcMain, Rectangle, screen } from 'electron'
|
import { BrowserWindow, app, ipcMain, Rectangle, screen } from 'electron'
|
||||||
import ElectronConfig = require('electron-config')
|
import ElectronConfig = require('electron-config')
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
import { loadConfig } from './config'
|
import { loadConfig } from './config'
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ export class Window {
|
|||||||
private windowConfig: ElectronConfig
|
private windowConfig: ElectronConfig
|
||||||
private windowBounds: Rectangle
|
private windowBounds: Rectangle
|
||||||
private closing = false
|
private closing = false
|
||||||
|
private lastVibrancy: {enabled: boolean, type?: string} | null = null
|
||||||
|
private disableVibrancyWhileDragging = false
|
||||||
|
|
||||||
get visible$ (): Observable<boolean> { return this.visible }
|
get visible$ (): Observable<boolean> { return this.visible }
|
||||||
|
|
||||||
@@ -46,6 +49,8 @@ export class Window {
|
|||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
preload: path.join(__dirname, 'sentry.js'),
|
||||||
|
backgroundThrottling: false,
|
||||||
},
|
},
|
||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
@@ -54,14 +59,14 @@ export class Window {
|
|||||||
|
|
||||||
if (this.windowBounds) {
|
if (this.windowBounds) {
|
||||||
Object.assign(bwOptions, this.windowBounds)
|
Object.assign(bwOptions, this.windowBounds)
|
||||||
const closestDisplay = screen.getDisplayNearestPoint( {x: this.windowBounds.x, y: this.windowBounds.y} )
|
const closestDisplay = screen.getDisplayNearestPoint( { x: this.windowBounds.x, y: this.windowBounds.y } )
|
||||||
|
|
||||||
const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height];
|
const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height]
|
||||||
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height];
|
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height]
|
||||||
|
|
||||||
if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) {
|
if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) {
|
||||||
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2;
|
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2
|
||||||
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2;
|
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +120,12 @@ export class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVibrancy (enabled: boolean, type?: string) {
|
setVibrancy (enabled: boolean, type?: string) {
|
||||||
|
this.lastVibrancy = { enabled, type }
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
if (parseFloat(os.release()) >= 10) {
|
if (parseFloat(os.release()) >= 10) {
|
||||||
let attribValue = AccentState.ACCENT_DISABLED
|
let attribValue = AccentState.ACCENT_DISABLED
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
|
if (type === 'fluent') {
|
||||||
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
|
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||||
} else {
|
} else {
|
||||||
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
|
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
|
||||||
@@ -129,6 +135,8 @@ export class Window {
|
|||||||
} else {
|
} else {
|
||||||
DwmEnableBlurBehindWindow(this.window, enabled)
|
DwmEnableBlurBehindWindow(this.window, enabled)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.window.setVibrancy(enabled ? 'dark' : null as any) // electron issue 20269
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +155,14 @@ export class Window {
|
|||||||
this.window.webContents.send(event, ...args)
|
this.window.webContents.send(event, ...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDestroyed() {
|
isDestroyed () {
|
||||||
return !this.window || this.window.isDestroyed();
|
return !this.window || this.window.isDestroyed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupWindowManagement () {
|
private setupWindowManagement () {
|
||||||
this.window.on('show', () => {
|
this.window.on('show', () => {
|
||||||
this.visible.next(true)
|
this.visible.next(true)
|
||||||
this.window.webContents.send('host:window-shown')
|
this.send('host:window-shown')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.window.on('hide', () => {
|
this.window.on('hide', () => {
|
||||||
@@ -164,20 +172,20 @@ export class Window {
|
|||||||
let moveSubscription = new Observable<void>(observer => {
|
let moveSubscription = new Observable<void>(observer => {
|
||||||
this.window.on('move', () => observer.next())
|
this.window.on('move', () => observer.next())
|
||||||
}).pipe(debounceTime(250)).subscribe(() => {
|
}).pipe(debounceTime(250)).subscribe(() => {
|
||||||
this.window.webContents.send('host:window-moved')
|
this.send('host:window-moved')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.window.on('closed', () => {
|
this.window.on('closed', () => {
|
||||||
moveSubscription.unsubscribe()
|
moveSubscription.unsubscribe()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
|
this.window.on('enter-full-screen', () => this.send('host:window-enter-full-screen'))
|
||||||
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
|
this.window.on('leave-full-screen', () => this.send('host:window-leave-full-screen'))
|
||||||
|
|
||||||
this.window.on('close', event => {
|
this.window.on('close', event => {
|
||||||
if (!this.closing) {
|
if (!this.closing) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.window.webContents.send('host:window-close-request')
|
this.send('host:window-close-request')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.windowConfig.set('windowBoundaries', this.windowBounds)
|
this.windowConfig.set('windowBoundaries', this.windowBounds)
|
||||||
@@ -200,6 +208,10 @@ export class Window {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.window.on('focus', () => {
|
||||||
|
this.send('host:window-focused')
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.on('window-focus', event => {
|
ipcMain.on('window-focus', event => {
|
||||||
if (!this.window || event.sender !== this.window.webContents) {
|
if (!this.window || event.sender !== this.window.webContents) {
|
||||||
return
|
return
|
||||||
@@ -287,6 +299,29 @@ export class Window {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.window.webContents.on('new-window', event => event.preventDefault())
|
this.window.webContents.on('new-window', event => event.preventDefault())
|
||||||
|
|
||||||
|
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
||||||
|
this.disableVibrancyWhileDragging = value
|
||||||
|
})
|
||||||
|
|
||||||
|
this.window.on('will-move', () => {
|
||||||
|
if (!this.lastVibrancy?.enabled || !this.disableVibrancyWhileDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timeout: number|null = null
|
||||||
|
const oldVibrancy = this.lastVibrancy
|
||||||
|
this.setVibrancy(false)
|
||||||
|
const onMove = () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.window.off('move', onMove)
|
||||||
|
this.setVibrancy(oldVibrancy.enabled, oldVibrancy.type)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
this.window.on('move', onMove)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroy () {
|
private destroy () {
|
||||||
|
@@ -28,27 +28,27 @@
|
|||||||
"electron-updater": "^4.2.0",
|
"electron-updater": "^4.2.0",
|
||||||
"fontmanager-redux": "0.4.0",
|
"fontmanager-redux": "0.4.0",
|
||||||
"js-yaml": "3.13.1",
|
"js-yaml": "3.13.1",
|
||||||
"keytar": "^5.0.0",
|
"keytar": "^5.2.0",
|
||||||
"mz": "^2.7.0",
|
"mz": "^2.7.0",
|
||||||
"ngx-toastr": "^10.2.0",
|
"ngx-toastr": "^10.2.0",
|
||||||
"node-pty": "^0.10.0-beta2",
|
"node-pty": "^0.10.0-beta2",
|
||||||
"npm": "6.9.0",
|
"npm": "6.9.0",
|
||||||
"path": "0.12.7",
|
"path": "0.12.7",
|
||||||
"rxjs": "^6.5.3",
|
"rxjs": "^6.5.4",
|
||||||
"rxjs-compat": "^6.5.3",
|
"rxjs-compat": "^6.5.4",
|
||||||
"yargs": "^15.0.2",
|
"yargs": "^15.1.0",
|
||||||
"zone.js": "^0.8.29"
|
"zone.js": "^0.8.29"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"macos-native-processlist": "^1.0.2",
|
"macos-native-processlist": "^1.0.2",
|
||||||
"windows-blurbehind": "^1.0.1",
|
"windows-blurbehind": "^1.0.1",
|
||||||
"windows-native-registry": "^1.0.15",
|
"windows-native-registry": "^1.0.17",
|
||||||
"windows-process-tree": "^0.2.4",
|
"windows-process-tree": "^0.2.4",
|
||||||
"windows-swca": "^2.0.2"
|
"windows-swca": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mz": "0.0.32",
|
"@types/mz": "0.0.32",
|
||||||
"@types/node": "12.7.12",
|
"@types/node": "12.7.12",
|
||||||
"node-abi": "^2.12.0"
|
"node-abi": "^2.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,33 +6,3 @@ import '@fortawesome/fontawesome-free/css/brands.css'
|
|||||||
import '@fortawesome/fontawesome-free/css/fontawesome.css'
|
import '@fortawesome/fontawesome-free/css/fontawesome.css'
|
||||||
import 'ngx-toastr/toastr.css'
|
import 'ngx-toastr/toastr.css'
|
||||||
import './preload.scss'
|
import './preload.scss'
|
||||||
|
|
||||||
import * as Raven from 'raven-js'
|
|
||||||
|
|
||||||
const SENTRY_DSN = 'https://4717a0a7ee0b4429bd3a0f06c3d7eec3@sentry.io/181876'
|
|
||||||
|
|
||||||
Raven.config(
|
|
||||||
SENTRY_DSN,
|
|
||||||
{
|
|
||||||
release: require('electron').remote.app.getVersion(),
|
|
||||||
dataCallback: (data: any) => {
|
|
||||||
const normalize = (filename: string) => {
|
|
||||||
const splitArray = filename.split('/')
|
|
||||||
return splitArray[splitArray.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
data.exception.values[0].stacktrace.frames.forEach((frame: any) => {
|
|
||||||
frame.filename = normalize(frame.filename)
|
|
||||||
})
|
|
||||||
|
|
||||||
data.culprit = data.exception.values[0].stacktrace.frames[0].filename
|
|
||||||
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
process.on('uncaughtException' as any, (err) => {
|
|
||||||
Raven.captureException(err as any)
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
|
@@ -7,7 +7,8 @@ import * as isDev from 'electron-is-dev'
|
|||||||
import './global.scss'
|
import './global.scss'
|
||||||
import './toastr.scss'
|
import './toastr.scss'
|
||||||
|
|
||||||
import { enableProdMode, NgModuleRef } from '@angular/core'
|
import { enableProdMode, NgModuleRef, ApplicationRef } from '@angular/core'
|
||||||
|
import { enableDebugTools } from '@angular/platform-browser'
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||||
|
|
||||||
import { getRootModule } from './app.module'
|
import { getRootModule } from './app.module'
|
||||||
@@ -37,7 +38,14 @@ async function bootstrap (plugins: PluginInfo[], safeMode = false): Promise<NgMo
|
|||||||
})
|
})
|
||||||
const module = getRootModule(pluginsModules)
|
const module = getRootModule(pluginsModules)
|
||||||
window['rootModule'] = module
|
window['rootModule'] = module
|
||||||
return platformBrowserDynamic().bootstrapModule(module)
|
return platformBrowserDynamic().bootstrapModule(module).then(moduleRef => {
|
||||||
|
if (isDev) {
|
||||||
|
const applicationRef = moduleRef.injector.get(ApplicationRef)
|
||||||
|
const componentRef = applicationRef.components[0]
|
||||||
|
enableDebugTools(componentRef)
|
||||||
|
}
|
||||||
|
return moduleRef
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findPlugins().then(async plugins => {
|
findPlugins().then(async plugins => {
|
||||||
|
@@ -21,8 +21,7 @@ if (process.env.TERMINUS_DEV) {
|
|||||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||||
|
|
||||||
const userPluginsPath = path.join(
|
const userPluginsPath = path.join(
|
||||||
require('electron').remote.app.getPath('appData'),
|
require('electron').remote.app.getPath('userData'),
|
||||||
'terminus',
|
|
||||||
'plugins',
|
'plugins',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -10,6 +10,10 @@
|
|||||||
background-image: none;
|
background-image: none;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
|
&.toast-error {
|
||||||
|
background-color: #BD362F;
|
||||||
|
}
|
||||||
|
|
||||||
&.toast-info {
|
&.toast-info {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ module.exports = {
|
|||||||
target: 'node',
|
target: 'node',
|
||||||
entry: {
|
entry: {
|
||||||
'index.ignore': 'file-loader?name=index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
'index.ignore': 'file-loader?name=index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
||||||
|
sentry: path.resolve(__dirname, 'lib/sentry.ts'),
|
||||||
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
|
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
|
||||||
bundle: path.resolve(__dirname, 'src/entry.ts'),
|
bundle: path.resolve(__dirname, 'src/entry.ts'),
|
||||||
},
|
},
|
||||||
@@ -78,5 +79,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"renderer"'
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -45,5 +45,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"main"',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -1485,10 +1485,10 @@ keyboardevents-areequal@^0.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
||||||
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
|
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
|
||||||
|
|
||||||
keytar@^5.0.0:
|
keytar@^5.2.0:
|
||||||
version "5.0.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.0.0.tgz#c89b6b7a4608fd7af633d9f8474b1a7eb97cbe6f"
|
resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.2.0.tgz#7543190be98e2a751466096ce3ca1b1a4b8f39fe"
|
||||||
integrity sha512-a5UheK59YOlJf9i+2Osaj/kkH6mK0RCHVMtJ84u6ZfbfRIbOJ/H4b5VlOF/LgNHF6s78dRSBzZnvIuPiBKv6wg==
|
integrity sha512-vsIX6n2BgTwzbKOSPIiJ8YduwHlPEE/G5dkmZWXaQK9qiGZMQyhxlFA4O6vrvM5fsXTMgUOrODYAqgpfNSRLDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
nan "2.14.0"
|
nan "2.14.0"
|
||||||
prebuild-install "5.3.3"
|
prebuild-install "5.3.3"
|
||||||
@@ -1925,10 +1925,10 @@ ngx-toastr@^10.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
node-abi@^2.12.0, node-abi@^2.7.0:
|
node-abi@^2.15.0, node-abi@^2.7.0:
|
||||||
version "2.12.0"
|
version "2.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.12.0.tgz#40e9cfabdda1837863fa825e7dfa0b15686adf6f"
|
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.15.0.tgz#51d55cc711bd9e4a24a572ace13b9231945ccb10"
|
||||||
integrity sha512-VhPBXCIcvmo/5K8HPmnWJyyhvgKxnHTUMXR/XwGHV68+wrgkzST4UmQrY/XszSWA5dtnXpNp528zkcyJ/pzVcw==
|
integrity sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
semver "^5.4.1"
|
semver "^5.4.1"
|
||||||
|
|
||||||
@@ -2801,15 +2801,15 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
aproba "^1.1.1"
|
aproba "^1.1.1"
|
||||||
|
|
||||||
rxjs-compat@^6.5.3:
|
rxjs-compat@^6.5.4:
|
||||||
version "6.5.3"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.5.3.tgz#18440949b2678bf87a78a754009676b2c49183dc"
|
resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.5.4.tgz#03825692af3fe363e04c43f41ff4113d76bbd305"
|
||||||
integrity sha512-BIJX2yovz3TBpjJoAZyls2QYuU6ZiCaZ+U96SmxQpuSP/qDUfiXPKOVLbThBB2WZijNHkdTTJXKRwvv5Y48H7g==
|
integrity sha512-rkn+lbOHUQOurdd74J/hjmDsG9nFx0z66fvnbs8M95nrtKvNqCKdk7iZqdY51CGmDemTQk+kUPy4s8HVOHtkfA==
|
||||||
|
|
||||||
rxjs@^6.5.3:
|
rxjs@^6.5.4:
|
||||||
version "6.5.3"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
|
||||||
integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==
|
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
@@ -3397,10 +3397,10 @@ windows-blurbehind@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.1.tgz#ff098713873304e38330b2c54cc41bb369b587b9"
|
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.1.tgz#ff098713873304e38330b2c54cc41bb369b587b9"
|
||||||
integrity sha512-1HzHfCiM1ayrbACJu5qE9zELV24uX/tINT6kxaZwLY3rtQAoeav6x9z7LFHWoLaGDN/sYbnK+9Vk0cz7fsk5HQ==
|
integrity sha512-1HzHfCiM1ayrbACJu5qE9zELV24uX/tINT6kxaZwLY3rtQAoeav6x9z7LFHWoLaGDN/sYbnK+9Vk0cz7fsk5HQ==
|
||||||
|
|
||||||
windows-native-registry@^1.0.15:
|
windows-native-registry@^1.0.17:
|
||||||
version "1.0.15"
|
version "1.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/windows-native-registry/-/windows-native-registry-1.0.15.tgz#02593331fb7dcab99ef3ac9dd71f2c71ae57b189"
|
resolved "https://registry.yarnpkg.com/windows-native-registry/-/windows-native-registry-1.0.17.tgz#d8cce48b364703a55c226690431b325114405022"
|
||||||
integrity sha512-uIsz1y3LrKPkphcZsezThz07FW7Vm00Zfa6ZU88rIo2zilOLE6Ui75jh6UkBAMso8xJeyvYbbcxF9kr4Zt8Iuw==
|
integrity sha512-u9Fp9TyDo5dvhlW6hYBOdHPETtAahXKxo3jeW5EXwNK7qa+nSNopQycN1drtBVWe3jpJXvyKpt9zrjiDd+u4JQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
nan "^2.14.0"
|
nan "^2.14.0"
|
||||||
|
|
||||||
@@ -3519,10 +3519,10 @@ yargs@^11.0.0:
|
|||||||
y18n "^3.2.1"
|
y18n "^3.2.1"
|
||||||
yargs-parser "^9.0.2"
|
yargs-parser "^9.0.2"
|
||||||
|
|
||||||
yargs@^15.0.2:
|
yargs@^15.1.0:
|
||||||
version "15.0.2"
|
version "15.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219"
|
||||||
integrity sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q==
|
integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui "^6.0.0"
|
cliui "^6.0.0"
|
||||||
decamelize "^1.2.0"
|
decamelize "^1.2.0"
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
cat > '/usr/bin/${executable}' << END
|
||||||
|
#!/bin/sh
|
||||||
|
'/opt/${productFilename}/${executable}' --no-sandbox $@
|
||||||
|
END
|
||||||
|
|
||||||
# Link to the binary
|
chmod +x '/usr/bin/${executable}'
|
||||||
ln -sf '/opt/${productFilename}/${executable}' '/usr/bin/${executable}'
|
|
||||||
|
60
electron-builder.yml
Normal file
60
electron-builder.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
appId: org.terminus
|
||||||
|
productName: Terminus
|
||||||
|
compression: normal
|
||||||
|
afterSign: "./build/mac/afterSignHook.js"
|
||||||
|
files:
|
||||||
|
- "**/*"
|
||||||
|
- dist
|
||||||
|
extraResources:
|
||||||
|
- builtin-plugins
|
||||||
|
- extras
|
||||||
|
publish:
|
||||||
|
- provider: github
|
||||||
|
|
||||||
|
win:
|
||||||
|
icon: "./build/windows/icon.ico"
|
||||||
|
artifactName: terminus-${version}-portable.${ext}
|
||||||
|
rfc3161TimeStampServer: http://sha256timestamp.ws.symantec.com/sha256/timestamp
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
artifactName: terminus-${version}-setup.${ext}
|
||||||
|
installerIcon: "./build/windows/icon.ico"
|
||||||
|
|
||||||
|
mac:
|
||||||
|
category: public.app-category.video
|
||||||
|
icon: "./build/mac/icon.icns"
|
||||||
|
artifactName: terminus-${version}-macos.${ext}
|
||||||
|
hardenedRuntime: true
|
||||||
|
entitlements: "./build/mac/entitlements.plist"
|
||||||
|
entitlementsInherit: "./build/mac/entitlements.plist"
|
||||||
|
extendInfo:
|
||||||
|
NSRequiresAquaSystemAppearance: false
|
||||||
|
pkg:
|
||||||
|
artifactName: terminus-${version}-macos.pkg
|
||||||
|
|
||||||
|
linux:
|
||||||
|
category: Utilities
|
||||||
|
icon: "./build/icons"
|
||||||
|
artifactName: terminus-${version}-linux.${ext}
|
||||||
|
executableArgs:
|
||||||
|
- "--no-sandbox"
|
||||||
|
snap:
|
||||||
|
plugs:
|
||||||
|
- default
|
||||||
|
- system-files
|
||||||
|
- system-observe
|
||||||
|
deb:
|
||||||
|
depends:
|
||||||
|
- gconf2
|
||||||
|
- gconf-service
|
||||||
|
- libnotify4
|
||||||
|
- libsecret-1-0
|
||||||
|
- libappindicator1
|
||||||
|
- libxtst6
|
||||||
|
- libnss3
|
||||||
|
afterInstall: build/linux/after-install.tpl
|
||||||
|
rpm:
|
||||||
|
depends:
|
||||||
|
- screen
|
||||||
|
- gnome-python2-gnomekeyring
|
127
package.json
127
package.json
@@ -1,30 +1,34 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.12.1",
|
||||||
|
"@sentry/cli": "^1.49.0",
|
||||||
|
"@sentry/electron": "^1.0.0",
|
||||||
"@types/electron-config": "^3.2.2",
|
"@types/electron-config": "^3.2.2",
|
||||||
"@types/electron-debug": "^2.1.0",
|
"@types/electron-debug": "^2.1.0",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/node": "12.7.12",
|
"@types/node": "12.7.12",
|
||||||
"@types/webpack-env": "1.14.1",
|
"@types/webpack-env": "1.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.8.0",
|
"@typescript-eslint/eslint-plugin": "^2.13.0",
|
||||||
"@typescript-eslint/parser": "^2.8.0",
|
"@typescript-eslint/parser": "^2.19.2",
|
||||||
"apply-loader": "2.0.0",
|
"apply-loader": "2.0.0",
|
||||||
"awesome-typescript-loader": "^5.0.0",
|
"awesome-typescript-loader": "^5.0.0",
|
||||||
"core-js": "^3.4.2",
|
"core-js": "^3.6.4",
|
||||||
"cross-env": "6.0.3",
|
"cross-env": "7.0.0",
|
||||||
"css-loader": "3.2.0",
|
"css-loader": "3.4.2",
|
||||||
"electron": "^7.1.2",
|
"electron": "^8.0.0",
|
||||||
"electron-builder": "22.1.0",
|
"electron-builder": "22.3.2",
|
||||||
"electron-installer-snap": "^4.1.0",
|
"electron-download": "^4.1.1",
|
||||||
|
"electron-installer-snap": "^5.0.0",
|
||||||
"electron-notarize": "^0.1.1",
|
"electron-notarize": "^0.1.1",
|
||||||
"electron-rebuild": "^1.8.5",
|
"electron-rebuild": "^1.9.0",
|
||||||
"eslint": "^6.7.1",
|
"eslint": "^6.8.0",
|
||||||
"file-loader": "^4.3.0",
|
"eslint-plugin-import": "^2.20.0",
|
||||||
|
"file-loader": "^5.0.2",
|
||||||
"graceful-fs": "^4.2.2",
|
"graceful-fs": "^4.2.2",
|
||||||
"html-loader": "0.5.5",
|
"html-loader": "0.5.5",
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"node-abi": "^2.12.0",
|
"node-abi": "^2.15.0",
|
||||||
"node-gyp": "^6.0.1",
|
"node-gyp": "^6.1.0",
|
||||||
"node-sass": "^4.13.0",
|
"node-sass": "^4.13.0",
|
||||||
"npmlog": "4.1.2",
|
"npmlog": "4.1.2",
|
||||||
"npx": "^10.2.0",
|
"npx": "^10.2.0",
|
||||||
@@ -33,95 +37,25 @@
|
|||||||
"pug-lint": "^2.6.0",
|
"pug-lint": "^2.6.0",
|
||||||
"pug-loader": "^2.4.0",
|
"pug-loader": "^2.4.0",
|
||||||
"pug-static-loader": "2.0.0",
|
"pug-static-loader": "2.0.0",
|
||||||
"raven-js": "3.27.2",
|
"raw-loader": "4.0.0",
|
||||||
"raw-loader": "3.1.0",
|
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^8.0.0",
|
||||||
"shelljs": "0.8.3",
|
"shelljs": "0.8.3",
|
||||||
"source-code-pro": "^2.30.2",
|
"source-code-pro": "^2.30.2",
|
||||||
"source-sans-pro": "3.6.0",
|
"source-sans-pro": "3.6.0",
|
||||||
"style-loader": "^1.0.0",
|
"style-loader": "^1.1.3",
|
||||||
"svg-inline-loader": "^0.8.0",
|
"svg-inline-loader": "^0.8.0",
|
||||||
"to-string-loader": "1.1.6",
|
"to-string-loader": "1.1.6",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"typedoc": "^0.15.3",
|
"typedoc": "^0.16.7",
|
||||||
"typescript": "^3.6.4",
|
"typescript": "^3.7.5",
|
||||||
"url-loader": "^2.3.0",
|
"url-loader": "^3.0.0",
|
||||||
"val-loader": "2.0.1",
|
"val-loader": "2.1.0",
|
||||||
"webpack": "^5.0.0-beta.7",
|
"webpack": "^5.0.0-beta.12",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
"yaml-loader": "0.5.0"
|
"yaml-loader": "0.5.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"*/node-abi": "^2.8.0"
|
"*/node-abi": "^2.14.0"
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "org.terminus",
|
|
||||||
"productName": "Terminus",
|
|
||||||
"compression": "normal",
|
|
||||||
"afterSign": "./build/mac/afterSignHook.js",
|
|
||||||
"files": [
|
|
||||||
"**/*",
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"extraResources": [
|
|
||||||
"builtin-plugins",
|
|
||||||
"extras"
|
|
||||||
],
|
|
||||||
"win": {
|
|
||||||
"icon": "./build/windows/icon.ico",
|
|
||||||
"artifactName": "terminus-${version}-setup.exe",
|
|
||||||
"rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"artifactName": "terminus-${version}-setup.${ext}",
|
|
||||||
"installerIcon": "./build/windows/icon.ico"
|
|
||||||
},
|
|
||||||
"publish": [
|
|
||||||
{
|
|
||||||
"provider": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"portable": {
|
|
||||||
"artifactName": "terminus-${version}-portable.exe"
|
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"category": "public.app-category.video",
|
|
||||||
"icon": "./build/mac/icon.icns",
|
|
||||||
"artifactName": "terminus-${version}-macos.${ext}",
|
|
||||||
"hardenedRuntime": true,
|
|
||||||
"entitlements": "./build/mac/entitlements.plist",
|
|
||||||
"entitlementsInherit": "./build/mac/entitlements.plist",
|
|
||||||
"extendInfo": {
|
|
||||||
"NSRequiresAquaSystemAppearance": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pkg": {
|
|
||||||
"artifactName": "terminus-${version}-macos.pkg"
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"category": "Utilities",
|
|
||||||
"icon": "./build/icons",
|
|
||||||
"artifactName": "terminus-${version}-linux.${ext}"
|
|
||||||
},
|
|
||||||
"deb": {
|
|
||||||
"depends": [
|
|
||||||
"gconf2",
|
|
||||||
"gconf-service",
|
|
||||||
"libnotify4",
|
|
||||||
"libsecret-1-0",
|
|
||||||
"libappindicator1",
|
|
||||||
"libxtst6",
|
|
||||||
"libnss3"
|
|
||||||
],
|
|
||||||
"afterInstall": "build/linux/after-install.tpl"
|
|
||||||
},
|
|
||||||
"rpm": {
|
|
||||||
"depends": [
|
|
||||||
"screen",
|
|
||||||
"gnome-python2-gnomekeyring"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
|
"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",
|
||||||
@@ -130,11 +64,8 @@
|
|||||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
||||||
"prod": "cross-env TERMINUS_DEV=1 electron app",
|
"prod": "cross-env TERMINUS_DEV=1 electron app",
|
||||||
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src",
|
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src",
|
||||||
"lint": "eslint --ext ts */src",
|
"lint": "eslint --ext ts */src */lib",
|
||||||
"postinstall": "node ./scripts/install-deps.js"
|
"postinstall": "node ./scripts/install-deps.js"
|
||||||
},
|
},
|
||||||
"repository": "eugeny/terminus",
|
"repository": "eugeny/terminus"
|
||||||
"dependencies": {
|
|
||||||
"eslint-plugin-import": "^2.18.2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ const isCI = !!process.env.GITHUB_REF
|
|||||||
|
|
||||||
builder({
|
builder({
|
||||||
dir: true,
|
dir: true,
|
||||||
win: ['nsis', 'portable'],
|
win: ['nsis', 'zip'],
|
||||||
config: {
|
config: {
|
||||||
extraMetadata: {
|
extraMetadata: {
|
||||||
version: vars.version,
|
version: vars.version,
|
||||||
|
@@ -25,8 +25,5 @@ if (['darwin', 'linux'].includes(process.platform)) {
|
|||||||
for (let x of vars.builtinPlugins) {
|
for (let x of vars.builtinPlugins) {
|
||||||
sh.ln('-fs', '../' + x, x)
|
sh.ln('-fs', '../' + x, x)
|
||||||
}
|
}
|
||||||
for (let x of vars.bundledModules) {
|
|
||||||
sh.ln('-fs', '../app/node_modules/' + x, x)
|
|
||||||
}
|
|
||||||
sh.cd('..')
|
sh.cd('..')
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-community-color-schemes",
|
"name": "terminus-community-color-schemes",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "Community color schemes for Terminus",
|
"description": "Community color schemes for Terminus",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-core",
|
"name": "terminus-core",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "Terminus core",
|
"description": "Terminus core",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||||
|
export { TabHeaderComponent } from '../components/tabHeader.component'
|
||||||
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||||
export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider'
|
export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider'
|
||||||
@@ -18,3 +19,4 @@ export { HostAppService, Platform } from '../services/hostApp.service'
|
|||||||
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
||||||
export { ThemesService } from '../services/themes.service'
|
export { ThemesService } from '../services/themes.service'
|
||||||
export { TabsService } from '../services/tabs.service'
|
export { TabsService } from '../services/tabs.service'
|
||||||
|
export * from '../utils'
|
||||||
|
@@ -38,7 +38,7 @@ title-bar(
|
|||||||
button.btn.btn-secondary.btn-tab-bar(
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
[title]='button.title',
|
[title]='button.title',
|
||||||
(click)='button.click && button.click()',
|
(click)='button.click && button.click()',
|
||||||
[innerHTML]='sanitizeIcon(button.icon)',
|
[fastHtmlBind]='button.icon',
|
||||||
ngbDropdownToggle,
|
ngbDropdownToggle,
|
||||||
)
|
)
|
||||||
div(*ngIf='button.submenu', ngbDropdownMenu)
|
div(*ngIf='button.submenu', ngbDropdownMenu)
|
||||||
@@ -47,8 +47,11 @@ title-bar(
|
|||||||
(click)='item.click()',
|
(click)='item.click()',
|
||||||
ngbDropdownItem,
|
ngbDropdownItem,
|
||||||
)
|
)
|
||||||
.icon-wrapper([innerHTML]='sanitizeIcon(item.icon)')
|
.icon-wrapper(
|
||||||
.ml-3 {{item.title}}
|
*ngIf='hasIcons(button.submenuItems)',
|
||||||
|
[fastHtmlBind]='item.icon'
|
||||||
|
)
|
||||||
|
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
|
||||||
|
|
||||||
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ title-bar(
|
|||||||
button.btn.btn-secondary.btn-tab-bar(
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
[title]='button.title',
|
[title]='button.title',
|
||||||
(click)='button.click && button.click()',
|
(click)='button.click && button.click()',
|
||||||
[innerHTML]='sanitizeIcon(button.icon)',
|
[fastHtmlBind]='button.icon',
|
||||||
ngbDropdownToggle,
|
ngbDropdownToggle,
|
||||||
)
|
)
|
||||||
div(*ngIf='button.submenu', ngbDropdownMenu)
|
div(*ngIf='button.submenu', ngbDropdownMenu)
|
||||||
@@ -70,14 +73,17 @@ title-bar(
|
|||||||
(click)='item.click()',
|
(click)='item.click()',
|
||||||
ngbDropdownItem,
|
ngbDropdownItem,
|
||||||
)
|
)
|
||||||
.icon-wrapper([innerHTML]='sanitizeIcon(item.icon)')
|
.icon-wrapper(
|
||||||
.ml-3 {{item.title}}
|
*ngIf='hasIcons(button.submenuItems)',
|
||||||
|
[fastHtmlBind]='item.icon'
|
||||||
|
)
|
||||||
|
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
|
||||||
|
|
||||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||||
*ngIf='updatesAvailable',
|
*ngIf='updatesAvailable',
|
||||||
title='Update available - Click to install',
|
title='Update available - Click to install',
|
||||||
(click)='updateApp()',
|
(click)='updateApp()',
|
||||||
[innerHTML]='sanitizeIcon(updateIcon)'
|
[fastHtmlBind]='updateIcon'
|
||||||
)
|
)
|
||||||
|
|
||||||
window-controls.background(
|
window-controls.background(
|
||||||
|
@@ -64,6 +64,7 @@ $tab-border-radius: 4px;
|
|||||||
&>.drag-space {
|
&>.drag-space {
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
flex: 1 0 1%;
|
flex: 1 0 1%;
|
||||||
|
margin-top: 2px; // for window resizing
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
&.persistent {
|
&.persistent {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
||||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||||
import { DomSanitizer } from '@angular/platform-browser'
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
import { ElectronService } from '../services/electron.service'
|
import { ElectronService } from '../services/electron.service'
|
||||||
@@ -75,7 +74,6 @@ export class AppRootComponent {
|
|||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public app: AppService,
|
public app: AppService,
|
||||||
private domSanitizer: DomSanitizer,
|
|
||||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||||
log: LogService,
|
log: LogService,
|
||||||
ngbModal: NgbModal,
|
ngbModal: NgbModal,
|
||||||
@@ -109,6 +107,12 @@ export class AppRootComponent {
|
|||||||
if (hotkey === 'previous-tab') {
|
if (hotkey === 'previous-tab') {
|
||||||
this.app.previousTab()
|
this.app.previousTab()
|
||||||
}
|
}
|
||||||
|
if (hotkey === 'move-tab-left') {
|
||||||
|
this.app.moveSelectedTabLeft()
|
||||||
|
}
|
||||||
|
if (hotkey === 'move-tab-right') {
|
||||||
|
this.app.moveSelectedTabRight()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hotkey === 'toggle-fullscreen') {
|
if (hotkey === 'toggle-fullscreen') {
|
||||||
this.hostApp.toggleFullscreen()
|
this.hostApp.toggleFullscreen()
|
||||||
@@ -186,6 +190,7 @@ export class AppRootComponent {
|
|||||||
if (this.config.store.appearance.dock === 'off') {
|
if (this.config.store.appearance.dock === 'off') {
|
||||||
// not docked, visible
|
// not docked, visible
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
this.hostApp.getWindow().show()
|
||||||
this.hostApp.getWindow().focus()
|
this.hostApp.getWindow().focus()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -250,8 +255,8 @@ export class AppRootComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizeIcon (icon: string): any {
|
hasIcons (submenuItems: ToolbarButton[]): boolean {
|
||||||
return this.domSanitizer.bypassSecurityTrustHtml(icon || '')
|
return submenuItems.some(x => !!x.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
||||||
|
@@ -38,7 +38,7 @@ export abstract class BaseTabComponent {
|
|||||||
*/
|
*/
|
||||||
color: string|null = null
|
color: string|null = null
|
||||||
|
|
||||||
protected hasFocus = false
|
hasFocus = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ping this if your recovery state has been changed and you want
|
* Ping this if your recovery state has been changed and you want
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
.icon(tabindex='0', [class.active]='model', (keyup.space)='click()')
|
|
||||||
i.fas.fa-square.off
|
|
||||||
i.fas.fa-check-square.on
|
|
||||||
.text {{text}}
|
|
@@ -1,55 +0,0 @@
|
|||||||
:host {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 5px 0;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background: rgba(255,255,255,.05);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(255,255,255,.1);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.off {
|
|
||||||
color: rgba(0, 0, 0, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
position: relative;
|
|
||||||
flex: none;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -2px;
|
|
||||||
transition: 0.25s opacity;
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.on, &.active i.off {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.off, &.active i.on {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: auto;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,8 +4,12 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
|||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'checkbox',
|
selector: 'checkbox',
|
||||||
template: require('./checkbox.component.pug'),
|
template: `
|
||||||
styles: [require('./checkbox.component.scss')],
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" [(ngModel)]='model'>
|
||||||
|
<label class="custom-control-label">{{text}}</label>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true },
|
{ provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true },
|
||||||
],
|
],
|
||||||
|
@@ -3,3 +3,24 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep split-tab > .child {
|
||||||
|
position: absolute;
|
||||||
|
transition: 0.125s all;
|
||||||
|
opacity: .75;
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.minimized {
|
||||||
|
opacity: .1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.maximized {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 30px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -141,6 +141,8 @@ export interface SplitSpannerInfo {
|
|||||||
styles: [require('./splitTab.component.scss')],
|
styles: [require('./splitTab.component.scss')],
|
||||||
})
|
})
|
||||||
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||||
|
static DIRECTIONS: SplitDirection[] = ['t', 'r', 'b', 'l']
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
|
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
|
||||||
|
|
||||||
@@ -156,6 +158,7 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
_spanners: SplitSpannerInfo[] = []
|
_spanners: SplitSpannerInfo[] = []
|
||||||
|
|
||||||
private focusedTab: BaseTabComponent
|
private focusedTab: BaseTabComponent
|
||||||
|
private maximizedTab: BaseTabComponent|null = null
|
||||||
private hotkeysSubscription: Subscription
|
private hotkeysSubscription: Subscription
|
||||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||||
|
|
||||||
@@ -226,6 +229,13 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
case 'pane-nav-down':
|
case 'pane-nav-down':
|
||||||
this.navigate('b')
|
this.navigate('b')
|
||||||
break
|
break
|
||||||
|
case 'pane-maximize':
|
||||||
|
if (this.maximizedTab) {
|
||||||
|
this.maximize(null)
|
||||||
|
} else if (this.getAllTabs().length > 1) {
|
||||||
|
this.maximize(this.focusedTab)
|
||||||
|
}
|
||||||
|
break
|
||||||
case 'close-pane':
|
case 'close-pane':
|
||||||
this.removeTab(this.focusedTab)
|
this.removeTab(this.focusedTab)
|
||||||
break
|
break
|
||||||
@@ -261,6 +271,10 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
return this.focusedTab
|
return this.focusedTab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMaximizedTab (): BaseTabComponent|null {
|
||||||
|
return this.maximizedTab
|
||||||
|
}
|
||||||
|
|
||||||
focus (tab: BaseTabComponent) {
|
focus (tab: BaseTabComponent) {
|
||||||
this.focusedTab = tab
|
this.focusedTab = tab
|
||||||
for (const x of this.getAllTabs()) {
|
for (const x of this.getAllTabs()) {
|
||||||
@@ -272,6 +286,15 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
tab.emitFocused()
|
tab.emitFocused()
|
||||||
this.focusChanged.next(tab)
|
this.focusChanged.next(tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.maximizedTab !== tab) {
|
||||||
|
this.maximizedTab = null
|
||||||
|
}
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
maximize (tab: BaseTabComponent|null) {
|
||||||
|
this.maximizedTab = tab
|
||||||
this.layout()
|
this.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +459,13 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
this.splitAdjusted.next(spanner)
|
this.splitAdjusted.next(spanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
super.destroy()
|
||||||
|
for (const x of this.getAllTabs()) {
|
||||||
|
x.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private attachTabView (tab: BaseTabComponent) {
|
private attachTabView (tab: BaseTabComponent) {
|
||||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
this.viewRefs.set(tab, ref)
|
this.viewRefs.set(tab, ref)
|
||||||
@@ -486,13 +516,21 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
this.layoutInternal(child, childX, childY, childW, childH)
|
this.layoutInternal(child, childX, childY, childW, childH)
|
||||||
} else {
|
} else {
|
||||||
const element = this.viewRefs.get(child)!.rootNodes[0]
|
const element = this.viewRefs.get(child)!.rootNodes[0]
|
||||||
element.style.position = 'absolute'
|
element.classList.toggle('child', true)
|
||||||
|
element.classList.toggle('maximized', child === this.maximizedTab)
|
||||||
|
element.classList.toggle('minimized', this.maximizedTab && child !== this.maximizedTab)
|
||||||
|
element.classList.toggle('focused', child === this.focusedTab)
|
||||||
element.style.left = `${childX}%`
|
element.style.left = `${childX}%`
|
||||||
element.style.top = `${childY}%`
|
element.style.top = `${childY}%`
|
||||||
element.style.width = `${childW}%`
|
element.style.width = `${childW}%`
|
||||||
element.style.height = `${childH}%`
|
element.style.height = `${childH}%`
|
||||||
|
|
||||||
element.style.opacity = child === this.focusedTab ? 1 : 0.75
|
if (child === this.maximizedTab) {
|
||||||
|
element.style.left = '5%'
|
||||||
|
element.style.top = '5%'
|
||||||
|
element.style.width = '90%'
|
||||||
|
element.style.height = '90%'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
offset += sizes[i]
|
offset += sizes[i]
|
||||||
|
|
||||||
@@ -526,6 +564,10 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
while (root.ratios.length < root.children.length) {
|
||||||
|
root.ratios.push(1)
|
||||||
|
}
|
||||||
|
root.normalize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,13 +16,17 @@ export class SplitTabSpannerComponent {
|
|||||||
@HostBinding('class.v') isVertical = true
|
@HostBinding('class.v') isVertical = true
|
||||||
@HostBinding('style.left') cssLeft: string
|
@HostBinding('style.left') cssLeft: string
|
||||||
@HostBinding('style.top') cssTop: string
|
@HostBinding('style.top') cssTop: string
|
||||||
@HostBinding('style.width') cssWidth: string
|
@HostBinding('style.width') cssWidth: string | null
|
||||||
@HostBinding('style.height') cssHeight: string
|
@HostBinding('style.height') cssHeight: string | null
|
||||||
private marginOffset = -5
|
private marginOffset = -5
|
||||||
|
|
||||||
constructor (private element: ElementRef) { }
|
constructor (private element: ElementRef) { }
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
|
this.element.nativeElement.addEventListener('dblclick', () => {
|
||||||
|
this.reset()
|
||||||
|
})
|
||||||
|
|
||||||
this.element.nativeElement.addEventListener('mousedown', (e: MouseEvent) => {
|
this.element.nativeElement.addEventListener('mousedown', (e: MouseEvent) => {
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
const start = this.isVertical ? e.pageY : e.pageX
|
const start = this.isVertical ? e.pageY : e.pageX
|
||||||
@@ -49,14 +53,16 @@ export class SplitTabSpannerComponent {
|
|||||||
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
|
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
|
||||||
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
|
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
|
||||||
|
|
||||||
this.container.ratios[this.index - 1] += diff
|
if (diff) {
|
||||||
this.container.ratios[this.index] -= diff
|
this.container.ratios[this.index - 1] += diff
|
||||||
this.change.emit()
|
this.container.ratios[this.index] -= diff
|
||||||
|
this.change.emit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('mouseup', offHandler)
|
document.addEventListener('mouseup', offHandler, { passive: true })
|
||||||
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
|
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
|
||||||
})
|
}, { passive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges () {
|
ngOnChanges () {
|
||||||
@@ -79,10 +85,17 @@ export class SplitTabSpannerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
const ratio = (this.container.ratios[this.index - 1] + this.container.ratios[this.index]) / 2
|
||||||
|
this.container.ratios[this.index - 1] = ratio
|
||||||
|
this.container.ratios[this.index] = ratio
|
||||||
|
this.change.emit()
|
||||||
|
}
|
||||||
|
|
||||||
private setDimensions (x: number, y: number, w: number, h: number) {
|
private setDimensions (x: number, y: number, w: number, h: number) {
|
||||||
this.cssLeft = `${x}%`
|
this.cssLeft = `${x}%`
|
||||||
this.cssTop = `${y}%`
|
this.cssTop = `${y}%`
|
||||||
this.cssWidth = w ? `${w}%` : 'initial'
|
this.cssWidth = w ? `${w}%` : null
|
||||||
this.cssHeight = h ? `${h}%` : 'initial'
|
this.cssHeight = h ? `${h}%` : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ $tabs-height: 38px;
|
|||||||
cursor: -webkit-grab;
|
cursor: -webkit-grab;
|
||||||
|
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
width: 20px;
|
width: 22px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: 0.25s all;
|
transition: 0.25s all;
|
||||||
@@ -48,7 +48,7 @@ $tabs-height: 38px;
|
|||||||
width: $button-size;
|
width: $button-size;
|
||||||
height: $button-size;
|
height: $button-size;
|
||||||
border-radius: $button-size / 2;
|
border-radius: $button-size / 2;
|
||||||
line-height: $button-size * 0.9;
|
line-height: $button-size;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
||||||
|
@@ -16,55 +16,8 @@
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background: rgba(255,255,255,.05);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
|
||||||
$border-width: 2px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: $border-width solid rgba(255, 255, 255, .2);
|
|
||||||
padding: $padding;
|
|
||||||
height: $toggle-size + $border-width * 2 + $padding * 2;
|
|
||||||
width: $toggle-size * 2 + $border-width * 2 + $padding * 2;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 2px;
|
|
||||||
width: $toggle-size;
|
|
||||||
height: $toggle-size;
|
|
||||||
background: #475158;
|
|
||||||
top: $padding;
|
|
||||||
left: $padding;
|
|
||||||
transition: 0.25s left;
|
|
||||||
line-height: 19px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 10px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.25s opacity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active .body .toggle {
|
|
||||||
left: $toggle-size + $padding;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: white;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(255,255,255,.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,10 @@ import { CheckboxComponent } from './checkbox.component'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'toggle',
|
selector: 'toggle',
|
||||||
template: `
|
template: `
|
||||||
<div class="switch">
|
<div class="custom-control custom-switch">
|
||||||
<div class="body">
|
<input type="checkbox" class="custom-control-input" [(ngModel)]='model'>
|
||||||
<div class="toggle" [class.bg-primary]='model'>
|
<label class="custom-control-label"></label>
|
||||||
<i class="fa fa-check"></i>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
styles: [require('./toggle.component.scss')],
|
styles: [require('./toggle.component.scss')],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -16,6 +16,10 @@ hotkeys:
|
|||||||
previous-tab:
|
previous-tab:
|
||||||
- 'Ctrl-Shift-Left'
|
- 'Ctrl-Shift-Left'
|
||||||
- 'Ctrl-Shift-Tab'
|
- 'Ctrl-Shift-Tab'
|
||||||
|
move-tab-left:
|
||||||
|
- 'Ctrl-Shift-PageUp'
|
||||||
|
move-tab-right:
|
||||||
|
- 'Ctrl-Shift-PageDown'
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
@@ -50,5 +54,7 @@ hotkeys:
|
|||||||
- 'Ctrl-Alt-Up'
|
- 'Ctrl-Alt-Up'
|
||||||
pane-nav-left:
|
pane-nav-left:
|
||||||
- 'Ctrl-Alt-Left'
|
- 'Ctrl-Alt-Left'
|
||||||
|
pane-maximize:
|
||||||
|
- 'Ctrl-Alt-Enter'
|
||||||
close-pane: []
|
close-pane: []
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -14,6 +14,10 @@ hotkeys:
|
|||||||
- 'Ctrl-Tab'
|
- 'Ctrl-Tab'
|
||||||
previous-tab:
|
previous-tab:
|
||||||
- 'Ctrl-Shift-Tab'
|
- 'Ctrl-Shift-Tab'
|
||||||
|
move-tab-left:
|
||||||
|
- '⌘-Shift-Left'
|
||||||
|
move-tab-right:
|
||||||
|
- '⌘-Shift-Right'
|
||||||
tab-1:
|
tab-1:
|
||||||
- '⌘-1'
|
- '⌘-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
@@ -48,6 +52,8 @@ hotkeys:
|
|||||||
- '⌘-⌥-Up'
|
- '⌘-⌥-Up'
|
||||||
pane-nav-left:
|
pane-nav-left:
|
||||||
- '⌘-⌥-Left'
|
- '⌘-⌥-Left'
|
||||||
|
pane-maximize:
|
||||||
|
- '⌘-⌥-Enter'
|
||||||
close-pane:
|
close-pane:
|
||||||
- '⌘-Shift-W'
|
- '⌘-Shift-W'
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -5,6 +5,7 @@ hotkeys:
|
|||||||
- 'Ctrl+Space'
|
- 'Ctrl+Space'
|
||||||
toggle-fullscreen:
|
toggle-fullscreen:
|
||||||
- 'F11'
|
- 'F11'
|
||||||
|
- 'Alt-Enter'
|
||||||
close-tab:
|
close-tab:
|
||||||
- 'Ctrl-Shift-W'
|
- 'Ctrl-Shift-W'
|
||||||
toggle-last-tab: []
|
toggle-last-tab: []
|
||||||
@@ -16,6 +17,10 @@ hotkeys:
|
|||||||
previous-tab:
|
previous-tab:
|
||||||
- 'Ctrl-Shift-Left'
|
- 'Ctrl-Shift-Left'
|
||||||
- 'Ctrl-Shift-Tab'
|
- 'Ctrl-Shift-Tab'
|
||||||
|
move-tab-left:
|
||||||
|
- 'Ctrl-Shift-PageUp'
|
||||||
|
move-tab-right:
|
||||||
|
- 'Ctrl-Shift-PageDown'
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
@@ -50,5 +55,7 @@ hotkeys:
|
|||||||
- 'Ctrl-Alt-Up'
|
- 'Ctrl-Alt-Up'
|
||||||
pane-nav-left:
|
pane-nav-left:
|
||||||
- 'Ctrl-Alt-Left'
|
- 'Ctrl-Alt-Left'
|
||||||
|
pane-maximize:
|
||||||
|
- 'Ctrl-Alt-Enter'
|
||||||
close-pane: []
|
close-pane: []
|
||||||
pluginBlacklist: []
|
pluginBlacklist: []
|
||||||
|
@@ -8,7 +8,7 @@ appearance:
|
|||||||
frame: thin
|
frame: thin
|
||||||
css: '/* * { color: blue !important; } */'
|
css: '/* * { color: blue !important; } */'
|
||||||
opacity: 1.0
|
opacity: 1.0
|
||||||
vibrancy: false
|
vibrancy: true
|
||||||
vibrancyType: 'blur'
|
vibrancyType: 'blur'
|
||||||
enableAnalytics: true
|
enableAnalytics: true
|
||||||
enableWelcomeTab: true
|
enableWelcomeTab: true
|
||||||
|
14
terminus-core/src/directives/fastHtmlBind.directive.ts
Normal file
14
terminus-core/src/directives/fastHtmlBind.directive.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Directive, Input, ElementRef, OnChanges } from '@angular/core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Directive({
|
||||||
|
selector: '[fastHtmlBind]',
|
||||||
|
})
|
||||||
|
export class FastHtmlBindDirective implements OnChanges {
|
||||||
|
@Input() fastHtmlBind: string
|
||||||
|
constructor (private el: ElementRef) { }
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.el.nativeElement.innerHTML = this.fastHtmlBind || ''
|
||||||
|
}
|
||||||
|
}
|
@@ -37,6 +37,14 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
id: 'previous-tab',
|
id: 'previous-tab',
|
||||||
name: 'Previous tab',
|
name: 'Previous tab',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'move-tab-left',
|
||||||
|
name: 'Move tab to the left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'move-tab-right',
|
||||||
|
name: 'Move tab to the right',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tab-1',
|
id: 'tab-1',
|
||||||
name: 'Tab 1',
|
name: 'Tab 1',
|
||||||
@@ -93,6 +101,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
id: 'split-top',
|
id: 'split-top',
|
||||||
name: 'Split to the top',
|
name: 'Split to the top',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'pane-maximize',
|
||||||
|
name: 'Maximize the active pane',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'pane-nav-up',
|
id: 'pane-nav-up',
|
||||||
name: 'Focus the pane above',
|
name: 'Focus the pane above',
|
||||||
|
@@ -21,6 +21,7 @@ import { SplitTabSpannerComponent } from './components/splitTabSpanner.component
|
|||||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||||
|
|
||||||
import { AutofocusDirective } from './directives/autofocus.directive'
|
import { AutofocusDirective } from './directives/autofocus.directive'
|
||||||
|
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||||
|
|
||||||
import { HotkeyProvider } from './api/hotkeyProvider'
|
import { HotkeyProvider } from './api/hotkeyProvider'
|
||||||
import { ConfigProvider } from './api/configProvider'
|
import { ConfigProvider } from './api/configProvider'
|
||||||
@@ -80,6 +81,7 @@ const PROVIDERS = [
|
|||||||
RenameTabModalComponent,
|
RenameTabModalComponent,
|
||||||
SafeModeModalComponent,
|
SafeModeModalComponent,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
|
FastHtmlBindDirective,
|
||||||
SplitTabComponent,
|
SplitTabComponent,
|
||||||
SplitTabSpannerComponent,
|
SplitTabSpannerComponent,
|
||||||
WelcomeTabComponent,
|
WelcomeTabComponent,
|
||||||
|
@@ -83,6 +83,10 @@ export class AppService {
|
|||||||
this.startTabStorage()
|
this.startTabStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostApp.windowFocused$.subscribe(() => {
|
||||||
|
this._activeTab?.emitFocused()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
startTabStorage () {
|
startTabStorage () {
|
||||||
@@ -94,8 +98,13 @@ export class AppService {
|
|||||||
}, 30000)
|
}, 30000)
|
||||||
}
|
}
|
||||||
|
|
||||||
addTabRaw (tab: BaseTabComponent) {
|
addTabRaw (tab: BaseTabComponent, index: number|null = null) {
|
||||||
this.tabs.push(tab)
|
if (index !== null) {
|
||||||
|
this.tabs.splice(index, 0, tab)
|
||||||
|
} else {
|
||||||
|
this.tabs.push(tab)
|
||||||
|
}
|
||||||
|
|
||||||
this.selectTab(tab)
|
this.selectTab(tab)
|
||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
this.tabOpened.next(tab)
|
this.tabOpened.next(tab)
|
||||||
@@ -121,6 +130,11 @@ export class AppService {
|
|||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
this.tabClosed.next(tab)
|
this.tabClosed.next(tab)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tab instanceof SplitTabComponent) {
|
||||||
|
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
||||||
|
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,6 +183,17 @@ export class AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParentTab (tab: BaseTabComponent): SplitTabComponent|null {
|
||||||
|
for (const topLevelTab of this.tabs) {
|
||||||
|
if (topLevelTab instanceof SplitTabComponent) {
|
||||||
|
if (topLevelTab.getAllTabs().includes(tab)) {
|
||||||
|
return topLevelTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/** Switches between the current tab and the previously active one */
|
/** Switches between the current tab and the previously active one */
|
||||||
toggleLastTab () {
|
toggleLastTab () {
|
||||||
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
||||||
@@ -199,6 +224,35 @@ export class AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveSelectedTabLeft () {
|
||||||
|
if (this.tabs.length > 1) {
|
||||||
|
const tabIndex = this.tabs.indexOf(this._activeTab)
|
||||||
|
if (tabIndex > 0) {
|
||||||
|
this.swapTabs(this._activeTab, this.tabs[tabIndex - 1])
|
||||||
|
} else if (this.config.store.appearance.cycleTabs) {
|
||||||
|
this.swapTabs(this._activeTab, this.tabs[this.tabs.length - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelectedTabRight () {
|
||||||
|
if (this.tabs.length > 1) {
|
||||||
|
const tabIndex = this.tabs.indexOf(this._activeTab)
|
||||||
|
if (tabIndex < this.tabs.length - 1) {
|
||||||
|
this.swapTabs(this._activeTab, this.tabs[tabIndex + 1])
|
||||||
|
} else if (this.config.store.appearance.cycleTabs) {
|
||||||
|
this.swapTabs(this._activeTab, this.tabs[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swapTabs (a: BaseTabComponent, b: BaseTabComponent) {
|
||||||
|
const i1 = this.tabs.indexOf(a)
|
||||||
|
const i2 = this.tabs.indexOf(b)
|
||||||
|
this.tabs[i1] = b
|
||||||
|
this.tabs[i2] = a
|
||||||
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
emitTabsChanged () {
|
emitTabsChanged () {
|
||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
@@ -217,7 +271,7 @@ export class AppService {
|
|||||||
async duplicateTab (tab: BaseTabComponent) {
|
async duplicateTab (tab: BaseTabComponent) {
|
||||||
const dup = await this.tabsService.duplicate(tab)
|
const dup = await this.tabsService.duplicate(tab)
|
||||||
if (dup) {
|
if (dup) {
|
||||||
this.addTabRaw(dup)
|
this.addTabRaw(dup, this.tabs.indexOf(tab) + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -125,7 +125,22 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDefaults () {
|
getDefaults () {
|
||||||
return this.defaults
|
const cleanup = o => {
|
||||||
|
if (o instanceof Array) {
|
||||||
|
return o.map(cleanup)
|
||||||
|
} else if (o instanceof Object) {
|
||||||
|
const r = {}
|
||||||
|
for (const k of Object.keys(o)) {
|
||||||
|
if (k !== '__nonStructural') {
|
||||||
|
r[k] = cleanup(o[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
} else {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanup(this.defaults)
|
||||||
}
|
}
|
||||||
|
|
||||||
load (): void {
|
load (): void {
|
||||||
|
@@ -30,9 +30,14 @@ export class HomeBaseService {
|
|||||||
let body = `Version: ${this.appVersion}\n`
|
let body = `Version: ${this.appVersion}\n`
|
||||||
body += `Platform: ${os.platform()} ${os.release()}\n`
|
body += `Platform: ${os.platform()} ${os.release()}\n`
|
||||||
const label = {
|
const label = {
|
||||||
|
aix: 'OS: IBM AIX',
|
||||||
|
android: 'OS: Android',
|
||||||
darwin: 'OS: macOS',
|
darwin: 'OS: macOS',
|
||||||
windows: 'OS: Windows',
|
freebsd: 'OS: FreeBSD',
|
||||||
linux: 'OS: Linux',
|
linux: 'OS: Linux',
|
||||||
|
openbsd: 'OS: OpenBSD',
|
||||||
|
sunos: 'OS: Solaris',
|
||||||
|
win32: 'OS: Windows',
|
||||||
}[os.platform()]
|
}[os.platform()]
|
||||||
const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
||||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
||||||
|
@@ -4,6 +4,7 @@ import { Observable, Subject } from 'rxjs'
|
|||||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||||
import { ElectronService } from './electron.service'
|
import { ElectronService } from './electron.service'
|
||||||
import { Logger, LogService } from './log.service'
|
import { Logger, LogService } from './log.service'
|
||||||
|
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Linux, macOS, Windows,
|
Linux, macOS, Windows,
|
||||||
@@ -39,6 +40,7 @@ export class HostAppService {
|
|||||||
private configChangeBroadcast = new Subject<void>()
|
private configChangeBroadcast = new Subject<void>()
|
||||||
private windowCloseRequest = new Subject<void>()
|
private windowCloseRequest = new Subject<void>()
|
||||||
private windowMoved = new Subject<void>()
|
private windowMoved = new Subject<void>()
|
||||||
|
private windowFocused = new Subject<void>()
|
||||||
private displayMetricsChanged = new Subject<void>()
|
private displayMetricsChanged = new Subject<void>()
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
private windowId: number
|
private windowId: number
|
||||||
@@ -85,6 +87,8 @@ export class HostAppService {
|
|||||||
|
|
||||||
get windowMoved$ (): Observable<void> { return this.windowMoved }
|
get windowMoved$ (): Observable<void> { return this.windowMoved }
|
||||||
|
|
||||||
|
get windowFocused$ (): Observable<void> { return this.windowFocused }
|
||||||
|
|
||||||
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
|
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
|
||||||
|
|
||||||
private constructor (
|
private constructor (
|
||||||
@@ -128,6 +132,10 @@ export class HostAppService {
|
|||||||
this.zone.run(() => this.windowMoved.next())
|
this.zone.run(() => this.windowMoved.next())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
electron.ipcRenderer.on('host:window-focused', () => {
|
||||||
|
this.zone.run(() => this.windowFocused.next())
|
||||||
|
})
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:display-metrics-changed', () => {
|
electron.ipcRenderer.on('host:display-metrics-changed', () => {
|
||||||
this.zone.run(() => this.displayMetricsChanged.next())
|
this.zone.run(() => this.displayMetricsChanged.next())
|
||||||
})
|
})
|
||||||
@@ -157,6 +165,14 @@ export class HostAppService {
|
|||||||
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
|
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
|
||||||
this.configChangeBroadcast.next()
|
this.configChangeBroadcast.next()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED) &&
|
||||||
|
!isWindowsBuild(WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED)
|
||||||
|
) {
|
||||||
|
electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,14 +228,12 @@ export class HostAppService {
|
|||||||
*
|
*
|
||||||
* @param type `null`, or `fluent` when supported (Windowd only)
|
* @param type `null`, or `fluent` when supported (Windowd only)
|
||||||
*/
|
*/
|
||||||
setVibrancy (enable: boolean, type: string) {
|
setVibrancy (enable: boolean, type: string|null) {
|
||||||
|
if (!isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
|
||||||
|
type = null
|
||||||
|
}
|
||||||
document.body.classList.toggle('vibrant', enable)
|
document.body.classList.toggle('vibrant', enable)
|
||||||
if (this.platform === Platform.macOS) {
|
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
|
||||||
this.getWindow().setVibrancy(enable ? 'dark' : null as any) // electron issue 20269
|
|
||||||
}
|
|
||||||
if (this.platform === Platform.Windows) {
|
|
||||||
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle (title: string) {
|
setTitle (title: string) {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||||
import { stringifyKeySequence } from './hotkeys.util'
|
import { stringifyKeySequence } from './hotkeys.util'
|
||||||
import { ConfigService } from '../services/config.service'
|
import { ConfigService } from '../services/config.service'
|
||||||
@@ -20,8 +21,17 @@ interface EventBufferEntry {
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HotkeysService {
|
export class HotkeysService {
|
||||||
key = new EventEmitter<KeyboardEvent>()
|
key = new EventEmitter<KeyboardEvent>()
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
matchedHotkey = new EventEmitter<string>()
|
matchedHotkey = new EventEmitter<string>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for each recognized hotkey
|
||||||
|
*/
|
||||||
|
get hotkey$ (): Observable<string> { return this._hotkey }
|
||||||
|
|
||||||
globalHotkey = new EventEmitter<void>()
|
globalHotkey = new EventEmitter<void>()
|
||||||
|
private _hotkey = new Subject<string>()
|
||||||
private currentKeystrokes: EventBufferEntry[] = []
|
private currentKeystrokes: EventBufferEntry[] = []
|
||||||
private disabledLevel = 0
|
private disabledLevel = 0
|
||||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||||
@@ -49,6 +59,9 @@ export class HotkeysService {
|
|||||||
this.getHotkeyDescriptions().then(hotkeys => {
|
this.getHotkeyDescriptions().then(hotkeys => {
|
||||||
this.hotkeyDescriptions = hotkeys
|
this.hotkeyDescriptions = hotkeys
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +84,7 @@ export class HotkeysService {
|
|||||||
const matched = this.getCurrentFullyMatchedHotkey()
|
const matched = this.getCurrentFullyMatchedHotkey()
|
||||||
if (matched) {
|
if (matched) {
|
||||||
console.log('Matched hotkey', matched)
|
console.log('Matched hotkey', matched)
|
||||||
this.matchedHotkey.emit(matched)
|
this._hotkey.next(matched)
|
||||||
this.clearCurrentKeystrokes()
|
this.clearCurrentKeystrokes()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -8,7 +8,7 @@ import { HostAppService, Platform } from './hostApp.service'
|
|||||||
/* eslint-disable block-scoped-var */
|
/* eslint-disable block-scoped-var */
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
|
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -18,11 +18,18 @@ export class ShellIntegrationService {
|
|||||||
private automatorWorkflowsDestination: string
|
private automatorWorkflowsDestination: string
|
||||||
private registryKeys = [
|
private registryKeys = [
|
||||||
{
|
{
|
||||||
path: 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here',
|
path: 'Software\\Classes\\Directory\\Background\\shell\\Terminus',
|
||||||
|
value: 'Open Terminus here',
|
||||||
command: 'open "%V"',
|
command: 'open "%V"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'Software\\Classes\\*\\shell\\Paste path into Terminus',
|
path: 'SOFTWARE\\Classes\\Directory\\shell\\Terminus',
|
||||||
|
value: 'Open Terminus here',
|
||||||
|
command: 'open "%V"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Software\\Classes\\*\\shell\\Terminus',
|
||||||
|
value: 'Paste path into Terminus',
|
||||||
command: 'paste "%V"',
|
command: 'paste "%V"',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -61,9 +68,17 @@ export class ShellIntegrationService {
|
|||||||
for (const registryKey of this.registryKeys) {
|
for (const registryKey of this.registryKeys) {
|
||||||
wnr.createRegistryKey(wnr.HK.CU, registryKey.path)
|
wnr.createRegistryKey(wnr.HK.CU, registryKey.path)
|
||||||
wnr.createRegistryKey(wnr.HK.CU, registryKey.path + '\\command')
|
wnr.createRegistryKey(wnr.HK.CU, registryKey.path + '\\command')
|
||||||
|
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, '', wnr.REG.SZ, registryKey.value)
|
||||||
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, 'Icon', wnr.REG.SZ, exe)
|
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, 'Icon', wnr.REG.SZ, exe)
|
||||||
wnr.setRegistryValue(wnr.HK.CU, registryKey.path + '\\command', '', wnr.REG.SZ, exe + ' ' + registryKey.command)
|
wnr.setRegistryValue(wnr.HK.CU, registryKey.path + '\\command', '', wnr.REG.SZ, exe + ' ' + registryKey.command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here')) {
|
||||||
|
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here')
|
||||||
|
}
|
||||||
|
if(wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Terminus')) {
|
||||||
|
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Terminus')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,18 @@ export class TabRecoveryService {
|
|||||||
window.localStorage.tabsRecovery = JSON.stringify(
|
window.localStorage.tabsRecovery = JSON.stringify(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tabs
|
tabs
|
||||||
.map(tab => tab.getRecoveryToken())
|
.map(tab => {
|
||||||
|
let token = tab.getRecoveryToken()
|
||||||
|
if (token) {
|
||||||
|
token = token.then(r => {
|
||||||
|
if (r) {
|
||||||
|
r.tabColor = tab.color
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
})
|
||||||
.filter(token => !!token)
|
.filter(token => !!token)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -31,7 +42,9 @@ export class TabRecoveryService {
|
|||||||
for (const provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
for (const provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
||||||
try {
|
try {
|
||||||
const tab = await provider.recover(token)
|
const tab = await provider.recover(token)
|
||||||
if (tab) {
|
if (tab !== null) {
|
||||||
|
tab.options = tab.options || {}
|
||||||
|
tab.options.color = token.tabColor || null
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -8,6 +8,7 @@ import { Injectable } from '@angular/core'
|
|||||||
import { Logger, LogService } from './log.service'
|
import { Logger, LogService } from './log.service'
|
||||||
import { ElectronService } from './electron.service'
|
import { ElectronService } from './electron.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
|
import { AppUpdater } from 'electron-updater'
|
||||||
|
|
||||||
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
||||||
|
|
||||||
@@ -18,21 +19,27 @@ export class UpdaterService {
|
|||||||
private downloaded: Promise<boolean>
|
private downloaded: Promise<boolean>
|
||||||
private electronUpdaterAvailable = true
|
private electronUpdaterAvailable = true
|
||||||
private updateURL: string
|
private updateURL: string
|
||||||
private autoUpdater
|
private autoUpdater: AppUpdater
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
log: LogService,
|
log: LogService,
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
config: ConfigService,
|
private config: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('updater')
|
this.logger = log.create('updater')
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
this.electronUpdaterAvailable = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.autoUpdater = electron.remote.require('electron-updater').autoUpdater
|
this.autoUpdater = electron.remote.require('electron-updater').autoUpdater
|
||||||
|
|
||||||
this.autoUpdater.autoInstallOnAppQuit = !!config.store.enableAutomaticUpdates
|
this.autoUpdater.autoInstallOnAppQuit = !!config.store.enableAutomaticUpdates
|
||||||
|
|
||||||
this.autoUpdater.on('update-available', () => {
|
this.autoUpdater.on('update-available', () => {
|
||||||
this.logger.info('Update available')
|
this.logger.info('Update available')
|
||||||
|
this.autoUpdater.downloadUpdate()
|
||||||
})
|
})
|
||||||
this.autoUpdater.once('update-not-available', () => {
|
this.autoUpdater.once('update-not-available', () => {
|
||||||
this.logger.info('No updates')
|
this.logger.info('No updates')
|
||||||
@@ -42,9 +49,8 @@ export class UpdaterService {
|
|||||||
this.autoUpdater.once('update-downloaded', () => resolve(true))
|
this.autoUpdater.once('update-downloaded', () => resolve(true))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.logger.debug('Checking for updates')
|
if (config.store.enableAutomaticUpdates && this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) {
|
||||||
|
this.logger.debug('Checking for updates')
|
||||||
if (this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) {
|
|
||||||
try {
|
try {
|
||||||
this.autoUpdater.checkForUpdates()
|
this.autoUpdater.checkForUpdates()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -55,6 +61,9 @@ export class UpdaterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async check (): Promise<boolean> {
|
async check (): Promise<boolean> {
|
||||||
|
if (!this.config.store.enableAutomaticUpdates) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!this.electronUpdaterAvailable) {
|
if (!this.electronUpdaterAvailable) {
|
||||||
this.logger.debug('Checking for updates through fallback method.')
|
this.logger.debug('Checking for updates through fallback method.')
|
||||||
const response = await axios.get(UPDATES_URL)
|
const response = await axios.get(UPDATES_URL)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
import { AppService } from './services/app.service'
|
import { AppService } from './services/app.service'
|
||||||
import { BaseTabComponent } from './components/baseTab.component'
|
import { BaseTabComponent } from './components/baseTab.component'
|
||||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||||
@@ -16,39 +17,49 @@ export class CloseContextMenu extends TabContextMenuItemProvider {
|
|||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||||
return [
|
let items = [
|
||||||
{
|
{
|
||||||
label: 'Close',
|
label: 'Close',
|
||||||
click: () => this.zone.run(() => {
|
click: () => this.zone.run(() => {
|
||||||
this.app.closeTab(tab, true)
|
if (this.app.tabs.includes(tab)) {
|
||||||
}),
|
this.app.closeTab(tab, true)
|
||||||
},
|
} else {
|
||||||
{
|
tab.destroy()
|
||||||
label: 'Close other tabs',
|
|
||||||
click: () => this.zone.run(() => {
|
|
||||||
for (const t of this.app.tabs.filter(x => x !== tab)) {
|
|
||||||
this.app.closeTab(t, true)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close tabs to the right',
|
|
||||||
click: () => this.zone.run(() => {
|
|
||||||
for (const t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
|
|
||||||
this.app.closeTab(t, true)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close tabs to the left',
|
|
||||||
click: () => this.zone.run(() => {
|
|
||||||
for (const t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
|
|
||||||
this.app.closeTab(t, true)
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
if (tabHeader) {
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
{
|
||||||
|
label: 'Close other tabs',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (const t of this.app.tabs.filter(x => x !== tab)) {
|
||||||
|
this.app.closeTab(t, true)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close tabs to the right',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (const t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
|
||||||
|
this.app.closeTab(t, true)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close tabs to the left',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (const t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
|
||||||
|
this.app.closeTab(t, true)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,28 +86,31 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||||
return [
|
if (tabHeader) {
|
||||||
{
|
return [
|
||||||
label: 'Rename',
|
{
|
||||||
click: () => this.zone.run(() => tabHeader && tabHeader.showRenameTabModal()),
|
label: 'Rename',
|
||||||
},
|
click: () => this.zone.run(() => tabHeader?.showRenameTabModal()),
|
||||||
{
|
},
|
||||||
label: 'Duplicate',
|
{
|
||||||
click: () => this.zone.run(() => this.app.duplicateTab(tab)),
|
label: 'Duplicate',
|
||||||
},
|
click: () => this.zone.run(() => this.app.duplicateTab(tab)),
|
||||||
{
|
},
|
||||||
label: 'Color',
|
{
|
||||||
sublabel: COLORS.find(x => x.value === tab.color)!.name,
|
label: 'Color',
|
||||||
submenu: COLORS.map(color => ({
|
sublabel: COLORS.find(x => x.value === tab.color)!.name,
|
||||||
label: color.name,
|
submenu: COLORS.map(color => ({
|
||||||
type: 'radio',
|
label: color.name,
|
||||||
checked: tab.color === color.value,
|
type: 'radio',
|
||||||
click: () => this.zone.run(() => {
|
checked: tab.color === color.value,
|
||||||
tab.color = color.value
|
click: () => this.zone.run(() => {
|
||||||
}),
|
tab.color = color.value
|
||||||
})) as Electron.MenuItemConstructorOptions[],
|
}),
|
||||||
},
|
})) as Electron.MenuItemConstructorOptions[],
|
||||||
]
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,36 +126,61 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
|||||||
|
|
||||||
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||||
const process = await tab.getCurrentProcess()
|
const process = await tab.getCurrentProcess()
|
||||||
if (process) {
|
let items: Electron.MenuItemConstructorOptions[] = []
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'process-name',
|
|
||||||
enabled: false,
|
|
||||||
label: 'Current process: ' + process.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Notify when done',
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: (tab as any).__completionNotificationEnabled,
|
|
||||||
click: () => this.zone.run(() => {
|
|
||||||
(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
|
|
||||||
|
|
||||||
if ((tab as any).__completionNotificationEnabled) {
|
const extTab: (BaseTabComponent & { __completionNotificationEnabled?: boolean, __outputNotificationSubscription?: Subscription|null }) = tab
|
||||||
this.app.observeTabCompletion(tab).subscribe(() => {
|
|
||||||
new Notification('Process completed', {
|
if (process) {
|
||||||
body: process.name,
|
items.push({
|
||||||
}).addEventListener('click', () => {
|
id: 'process-name',
|
||||||
this.app.selectTab(tab)
|
enabled: false,
|
||||||
})
|
label: 'Current process: ' + process.name,
|
||||||
;(tab as any).__completionNotificationEnabled = false
|
})
|
||||||
|
items.push({
|
||||||
|
label: 'Notify when done',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: extTab.__completionNotificationEnabled,
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
extTab.__completionNotificationEnabled = !extTab.__completionNotificationEnabled
|
||||||
|
|
||||||
|
if (extTab.__completionNotificationEnabled) {
|
||||||
|
this.app.observeTabCompletion(tab).subscribe(() => {
|
||||||
|
new Notification('Process completed', {
|
||||||
|
body: process.name,
|
||||||
|
}).addEventListener('click', () => {
|
||||||
|
this.app.selectTab(tab)
|
||||||
})
|
})
|
||||||
} else {
|
extTab.__completionNotificationEnabled = false
|
||||||
this.app.stopObservingTabCompletion(tab)
|
})
|
||||||
}
|
} else {
|
||||||
}),
|
this.app.stopObservingTabCompletion(tab)
|
||||||
},
|
}
|
||||||
]
|
}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return []
|
items.push({
|
||||||
|
label: 'Notify on activity',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: !!extTab.__outputNotificationSubscription,
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
if (extTab.__outputNotificationSubscription) {
|
||||||
|
extTab.__outputNotificationSubscription.unsubscribe()
|
||||||
|
extTab.__outputNotificationSubscription = null
|
||||||
|
} else {
|
||||||
|
extTab.__outputNotificationSubscription = tab.activity$.subscribe(active => {
|
||||||
|
if (extTab.__outputNotificationSubscription && active) {
|
||||||
|
extTab.__outputNotificationSubscription.unsubscribe()
|
||||||
|
extTab.__outputNotificationSubscription = null
|
||||||
|
new Notification('Tab activity', {
|
||||||
|
body: tab.title,
|
||||||
|
}).addEventListener('click', () => {
|
||||||
|
this.app.selectTab(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,4 +16,8 @@ app-root {
|
|||||||
terminaltab .content {
|
terminaltab .content {
|
||||||
margin: 5px !important;
|
margin: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssh-tab .content {
|
||||||
|
margin: 5px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,103 +1,11 @@
|
|||||||
$tab-border-radius: 5px;
|
@import "./theme.vars";
|
||||||
|
|
||||||
|
// ---------
|
||||||
|
|
||||||
|
|
||||||
$button-hover-bg: rgba(0, 0, 0, .25);
|
$button-hover-bg: rgba(0, 0, 0, .25);
|
||||||
$button-active-bg: rgba(0, 0, 0, .5);
|
$button-active-bg: rgba(0, 0, 0, .5);
|
||||||
|
|
||||||
|
|
||||||
$white: #fff !default;
|
|
||||||
$black: #000 !default;
|
|
||||||
$red: #d9534f !default;
|
|
||||||
$orange: #f0ad4e !default;
|
|
||||||
$yellow: #ffd500 !default;
|
|
||||||
$green: #5cb85c !default;
|
|
||||||
$blue: #0275d8 !default;
|
|
||||||
$teal: #5bc0de !default;
|
|
||||||
$pink: #ff5b77 !default;
|
|
||||||
$purple: #613d7c !default;
|
|
||||||
|
|
||||||
$theme-colors: (
|
|
||||||
"primary": $blue,
|
|
||||||
"secondary": #394b5d
|
|
||||||
);
|
|
||||||
|
|
||||||
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
|
||||||
$content-bg-solid: #1D272D;
|
|
||||||
$body-bg: #131d27;
|
|
||||||
$body-bg2: #20333e;
|
|
||||||
|
|
||||||
$body-color: #ccc;
|
|
||||||
$font-family-sans-serif: "Source Sans Pro";
|
|
||||||
$font-family-monospace: "Source Code Pro";
|
|
||||||
$font-size-base: 14rem / 16;
|
|
||||||
|
|
||||||
$btn-border-radius: 0;
|
|
||||||
$btn-secondary-color: #ccc;
|
|
||||||
$btn-secondary-bg: #222;
|
|
||||||
$btn-secondary-border: #444;
|
|
||||||
|
|
||||||
//$btn-warning-bg: rgba($orange, .5);
|
|
||||||
|
|
||||||
|
|
||||||
$nav-tabs-border-width: 0;
|
|
||||||
$nav-tabs-border-radius: 0;
|
|
||||||
$nav-tabs-link-hover-border-color: $body-bg;
|
|
||||||
$nav-tabs-active-link-hover-color: $white;
|
|
||||||
$nav-tabs-active-link-hover-bg: $blue;
|
|
||||||
$nav-tabs-active-link-hover-border-color: darken($blue, 30%);
|
|
||||||
$nav-pills-border-radius: 0;
|
|
||||||
|
|
||||||
$input-bg: #111;
|
|
||||||
$input-disabled-bg: #333;
|
|
||||||
|
|
||||||
$input-color: $body-color;
|
|
||||||
$input-color-placeholder: #333;
|
|
||||||
$input-border-color: #344;
|
|
||||||
$input-border-width: 1px;
|
|
||||||
//$input-box-shadow: inset 0 1px 1px rgba($black,.075);
|
|
||||||
$input-border-radius: 0;
|
|
||||||
$custom-select-border-radius: 0;
|
|
||||||
$input-bg-focus: $input-bg;
|
|
||||||
$input-border-focus: lighten($blue, 25%);
|
|
||||||
$input-focus-box-shadow: none;
|
|
||||||
$input-color-focus: $input-color;
|
|
||||||
$input-group-addon-bg: $body-bg;
|
|
||||||
$input-group-addon-border-color: $input-border-color;
|
|
||||||
|
|
||||||
$modal-content-bg: $content-bg-solid;
|
|
||||||
$modal-content-border-color: $body-bg;
|
|
||||||
$modal-header-border-color: transparent;
|
|
||||||
$modal-footer-border-color: transparent;
|
|
||||||
|
|
||||||
$popover-bg: $body-bg;
|
|
||||||
|
|
||||||
$dropdown-bg: $body-bg;
|
|
||||||
$dropdown-link-color: $body-color;
|
|
||||||
$dropdown-link-hover-color: white;
|
|
||||||
$dropdown-link-hover-bg: $body-bg2;
|
|
||||||
//$dropdown-link-active-color: $component-active-color;
|
|
||||||
//$dropdown-link-active-bg: $component-active-bg;
|
|
||||||
$dropdown-link-disabled-color: #333;
|
|
||||||
$dropdown-header-color: #333;
|
|
||||||
|
|
||||||
$list-group-color: $body-color;
|
|
||||||
$list-group-bg: rgba(255,255,255,.05);
|
|
||||||
$list-group-border-color: rgba(255,255,255,.1);
|
|
||||||
$list-group-hover-bg: rgba(255,255,255,.1);
|
|
||||||
$list-group-link-active-bg: rgba(255,255,255,.2);
|
|
||||||
|
|
||||||
$list-group-action-color: $body-color;
|
|
||||||
$list-group-action-bg: rgba(255,255,255,.05);
|
|
||||||
$list-group-action-active-bg: $list-group-link-active-bg;
|
|
||||||
|
|
||||||
$pre-bg: $dropdown-bg;
|
|
||||||
$pre-color: $dropdown-link-color;
|
|
||||||
|
|
||||||
$alert-danger-bg: $body-bg;
|
|
||||||
$alert-danger-text: $red;
|
|
||||||
$alert-danger-border: $red;
|
|
||||||
|
|
||||||
$headings-font-weight: lighter;
|
|
||||||
$headings-color: #eee;
|
|
||||||
|
|
||||||
@import '~bootstrap/scss/bootstrap.scss';
|
@import '~bootstrap/scss/bootstrap.scss';
|
||||||
|
|
||||||
window-controls {
|
window-controls {
|
||||||
@@ -236,13 +144,14 @@ settings-tab > ngb-tabset {
|
|||||||
border: none;
|
border: none;
|
||||||
padding: 10px 50px 10px 20px;
|
padding: 10px 50px 10px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,14 +219,6 @@ hotkey-input-modal {
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
background: $btn-secondary-bg;
|
|
||||||
.nav-link {
|
|
||||||
transition: 0.25s all;
|
|
||||||
border-bottom-color: $nav-tabs-border-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngb-tabset .tab-content {
|
ngb-tabset .tab-content {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
@@ -361,22 +262,10 @@ ngb-tabset .tab-content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select.form-control {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>");
|
|
||||||
background-position: 100% 50%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkbox i.on {
|
checkbox i.on {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle.active .body .toggle {
|
|
||||||
background: $blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .modal-footer {
|
.modal .modal-footer {
|
||||||
background: rgba(0, 0, 0, .25);
|
background: rgba(0, 0, 0, .25);
|
||||||
|
|
||||||
@@ -405,3 +294,101 @@ toggle.active .body .toggle {
|
|||||||
*::-webkit-resizer {
|
*::-webkit-resizer {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search-panel {
|
||||||
|
background: rgba(39, 49, 60, 0.65) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.disabled,
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-outline-secondary {
|
||||||
|
@include button-outline-variant(#9badb9, #fff);
|
||||||
|
&:hover:not([disabled]), &:active:not([disabled]), &.active:not([disabled]) {
|
||||||
|
background-color: #3f484e;
|
||||||
|
border-color: darken(#9badb9, 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
border-color: darken(#9badb9, 25%);
|
||||||
|
|
||||||
|
&.disabled,
|
||||||
|
&:disabled {
|
||||||
|
color: #9badb9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:not(:disabled):not(.disabled) {
|
||||||
|
&.active, &:active {
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(:disabled):not(.disabled) {
|
||||||
|
&.active, &:active {
|
||||||
|
background: #191e23;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
&:hover, &[aria-expanded=true], &:active, &.active {
|
||||||
|
color: $link-hover-color;
|
||||||
|
border-radius: $btn-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-expanded=true], &:active, &.active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn.active {
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&.nav-justified .nav-link {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
border: none;
|
||||||
|
border-bottom: $nav-tabs-border-width solid transparent;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
|
||||||
|
uib-tab-heading > i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include hover-focus {
|
||||||
|
color: $nav-tabs-link-active-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: $nav-link-disabled-color;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-child .nav-link {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active,
|
||||||
|
.nav-item.show .nav-link {
|
||||||
|
color: $nav-tabs-link-active-color;
|
||||||
|
border-color: $nav-tabs-link-active-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
187
terminus-core/src/theme.vars.scss
Normal file
187
terminus-core/src/theme.vars.scss
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
$white: #fff;
|
||||||
|
$gray-100: #f8f9fa;
|
||||||
|
$gray-200: #e9ecef;
|
||||||
|
$gray-300: #dee2e6;
|
||||||
|
$gray-400: #ced4da;
|
||||||
|
$gray-500: #adb5bd;
|
||||||
|
$gray-600: #6c757d;
|
||||||
|
$gray-700: #495057;
|
||||||
|
$gray-800: #343a40;
|
||||||
|
$gray-900: #212529;
|
||||||
|
$black: #000;
|
||||||
|
|
||||||
|
|
||||||
|
$red: #d9534f !default;
|
||||||
|
$orange: #f0ad4e !default;
|
||||||
|
$yellow: #ffd500 !default;
|
||||||
|
$green: #5cb85c !default;
|
||||||
|
$blue: #0275d8 !default;
|
||||||
|
$teal: #5bc0de !default;
|
||||||
|
$pink: #ff5b77 !default;
|
||||||
|
$purple: #613d7c !default;
|
||||||
|
|
||||||
|
|
||||||
|
@import "~bootstrap/scss/functions";
|
||||||
|
|
||||||
|
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
||||||
|
$content-bg-solid: #1D272D;
|
||||||
|
|
||||||
|
$table-bg: rgba(255,255,255,.05);
|
||||||
|
$table-bg-hover: rgba(255,255,255,.1);
|
||||||
|
$table-border-color: rgba(255,255,255,.1);
|
||||||
|
|
||||||
|
$theme-colors: (
|
||||||
|
primary: $blue,
|
||||||
|
secondary: #38434e,
|
||||||
|
success: $green,
|
||||||
|
info: $blue,
|
||||||
|
warning: $orange,
|
||||||
|
danger: $red,
|
||||||
|
light: $gray-300,
|
||||||
|
dark: $gray-800,
|
||||||
|
rare: $purple
|
||||||
|
);
|
||||||
|
|
||||||
|
$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;
|
||||||
|
$font-size-lg: 1.28rem;
|
||||||
|
$font-size-sm: .85rem;
|
||||||
|
|
||||||
|
$line-height-base: 1.6;
|
||||||
|
|
||||||
|
$headings-color: #ced9e2;
|
||||||
|
$headings-font-weight: lighter;
|
||||||
|
|
||||||
|
$input-btn-padding-y: .3rem;
|
||||||
|
$input-btn-padding-x: .9rem;
|
||||||
|
$input-btn-line-height: 1.6;
|
||||||
|
$input-btn-line-height-sm: 1.8;
|
||||||
|
$input-btn-line-height-lg: 1.8;
|
||||||
|
|
||||||
|
$btn-link-disabled-color: $gray-600;
|
||||||
|
$btn-focus-box-shadow: none;
|
||||||
|
|
||||||
|
$h4-font-size: 18px;
|
||||||
|
|
||||||
|
$link-color: $gray-400;
|
||||||
|
$link-hover-color: $white;
|
||||||
|
$link-hover-decoration: none;
|
||||||
|
|
||||||
|
$component-active-color: $white;
|
||||||
|
$component-active-bg: #2f3a42;
|
||||||
|
|
||||||
|
$list-group-bg: $table-bg;
|
||||||
|
$list-group-border-color: $table-border-color;
|
||||||
|
|
||||||
|
$list-group-item-padding-y: 0.8rem;
|
||||||
|
$list-group-item-padding-x: 1rem;
|
||||||
|
|
||||||
|
$list-group-hover-bg: $table-bg-hover;
|
||||||
|
$list-group-active-bg: rgba(255,255,255,.2);
|
||||||
|
$list-group-active-color: $component-active-color;
|
||||||
|
$list-group-active-border-color: translate;
|
||||||
|
|
||||||
|
$list-group-action-color: $body-color;
|
||||||
|
$list-group-action-hover-color: white;
|
||||||
|
|
||||||
|
$list-group-action-active-color: $component-active-color;
|
||||||
|
$list-group-action-active-bg: $list-group-active-bg;
|
||||||
|
|
||||||
|
$alert-padding-y: 0.9rem;
|
||||||
|
$alert-padding-x: 1.25rem;
|
||||||
|
|
||||||
|
$input-box-shadow: none;
|
||||||
|
|
||||||
|
$transition-base: all .15s ease-in-out;
|
||||||
|
$transition-fade: opacity .1s linear;
|
||||||
|
$transition-collapse: height .35s ease;
|
||||||
|
$btn-transition: all .15s ease-in-out;
|
||||||
|
|
||||||
|
$popover-bg: $body-bg;
|
||||||
|
$popover-body-color: $body-color;
|
||||||
|
$popover-header-bg: $table-bg-hover;
|
||||||
|
$popover-header-color: $headings-color;
|
||||||
|
$popover-arrow-color: $popover-bg;
|
||||||
|
$popover-max-width: 360px;
|
||||||
|
|
||||||
|
$btn-border-width: 2px;
|
||||||
|
|
||||||
|
$input-bg: #181e23;
|
||||||
|
$input-disabled-bg: #2e3235;
|
||||||
|
|
||||||
|
$input-color: #ddd;
|
||||||
|
$input-border-color: $input-bg;
|
||||||
|
$input-border-width: 2px;
|
||||||
|
|
||||||
|
$input-focus-bg: $input-bg;
|
||||||
|
$input-focus-border-color: rgba(171, 171, 171, 0.61);
|
||||||
|
$input-focus-color: $input-color;
|
||||||
|
|
||||||
|
$input-btn-focus-color: var(--focus-color);
|
||||||
|
$input-btn-focus-box-shadow: 0 0 0 2px $input-btn-focus-color;
|
||||||
|
|
||||||
|
$input-group-addon-color: $input-color;
|
||||||
|
$input-group-addon-bg: $input-bg;
|
||||||
|
$input-group-addon-border-color: transparent;
|
||||||
|
$input-group-btn-border-color: $input-bg;
|
||||||
|
|
||||||
|
$nav-tabs-border-radius: 0;
|
||||||
|
$nav-tabs-border-color: transparent;
|
||||||
|
$nav-tabs-border-width: 2px;
|
||||||
|
$nav-tabs-link-hover-border-color: transparent;
|
||||||
|
$nav-tabs-link-active-color: #eee;
|
||||||
|
$nav-tabs-link-active-bg: transparent;
|
||||||
|
$nav-tabs-link-active-border-color: #eee;
|
||||||
|
|
||||||
|
$navbar-padding-y: 0;
|
||||||
|
$navbar-padding-x: 0;
|
||||||
|
|
||||||
|
$dropdown-bg: $content-bg-solid;
|
||||||
|
$dropdown-color: $body-color;
|
||||||
|
$dropdown-border-width: 1px;
|
||||||
|
$dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175);
|
||||||
|
$dropdown-header-color: $gray-500;
|
||||||
|
|
||||||
|
$dropdown-link-color: $body-color;
|
||||||
|
$dropdown-link-hover-color: #eee;
|
||||||
|
$dropdown-link-hover-bg: rgba(255,255,255,.04);
|
||||||
|
$dropdown-link-active-color: white;
|
||||||
|
$dropdown-link-active-bg: rgba(0, 0, 0, .2);
|
||||||
|
$dropdown-item-padding-y: 0.5rem;
|
||||||
|
$dropdown-item-padding-x: 1.5rem;
|
||||||
|
|
||||||
|
|
||||||
|
$code-color: $orange;
|
||||||
|
$code-bg: rgba(0, 0, 0, .25);
|
||||||
|
$code-padding-y: 3px;
|
||||||
|
$code-padding-x: 5px;
|
||||||
|
$pre-bg: $dropdown-bg;
|
||||||
|
$pre-color: $dropdown-link-color;
|
||||||
|
|
||||||
|
$badge-font-size: 0.75rem;
|
||||||
|
$badge-font-weight: bold;
|
||||||
|
$badge-padding-y: 4px;
|
||||||
|
$badge-padding-x: 6px;
|
||||||
|
|
||||||
|
|
||||||
|
$custom-control-indicator-size: 1.2rem;
|
||||||
|
$custom-control-indicator-bg: $body-bg;
|
||||||
|
$custom-control-indicator-border-color: lighten($body-bg, 25%);
|
||||||
|
$custom-control-indicator-checked-bg: theme-color("primary");
|
||||||
|
$custom-control-indicator-checked-color: $body-bg;
|
||||||
|
$custom-control-indicator-checked-border-color: transparent;
|
||||||
|
$custom-control-indicator-active-bg: rgba(255, 255, 0, 0.5);
|
||||||
|
|
||||||
|
|
||||||
|
$modal-content-bg: $content-bg-solid;
|
||||||
|
$modal-content-border-color: $body-bg;
|
||||||
|
$modal-header-border-width: 0;
|
||||||
|
$modal-footer-border-color: #222;
|
||||||
|
$modal-footer-border-width: 1px;
|
||||||
|
$modal-content-border-width: 0;
|
@@ -3,6 +3,8 @@ import * as os from 'os'
|
|||||||
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
|
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
|
||||||
export const WIN_BUILD_CONPTY_STABLE = 18309
|
export const WIN_BUILD_CONPTY_STABLE = 18309
|
||||||
export const WIN_BUILD_WSL_EXE_DISTRO_FLAG = 17763
|
export const WIN_BUILD_WSL_EXE_DISTRO_FLAG = 17763
|
||||||
|
export const WIN_BUILD_FLUENT_BG_SUPPORTED = 17063
|
||||||
|
export const WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED = 18917
|
||||||
|
|
||||||
export function isWindowsBuild (build: number): boolean {
|
export function isWindowsBuild (build: number): boolean {
|
||||||
return process.platform === 'win32' && parseFloat(os.release()) >= 10 && parseInt(os.release().split('.')[2]) >= build
|
return process.platform === 'win32' && parseFloat(os.release()) >= 10 && parseInt(os.release().split('.')[2]) >= build
|
@@ -46,17 +46,16 @@ async@^2.6.1:
|
|||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
|
|
||||||
axios@^0.19.0:
|
axios@^0.19.0:
|
||||||
version "0.19.0"
|
version "0.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||||
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
|
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "1.5.10"
|
follow-redirects "1.5.10"
|
||||||
is-buffer "^2.0.2"
|
|
||||||
|
|
||||||
bootstrap@^4.1.3:
|
bootstrap@^4.1.3:
|
||||||
version "4.3.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac"
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01"
|
||||||
integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==
|
integrity sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==
|
||||||
|
|
||||||
builder-util-runtime@8.4.0:
|
builder-util-runtime@8.4.0:
|
||||||
version "8.4.0"
|
version "8.4.0"
|
||||||
@@ -118,9 +117,9 @@ colorspace@1.1.x:
|
|||||||
text-hex "1.0.x"
|
text-hex "1.0.x"
|
||||||
|
|
||||||
core-js@^3.1.2:
|
core-js@^3.1.2:
|
||||||
version "3.4.2"
|
version "3.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.2.tgz#ee2b1a60b50388d8ddcda8cdb44a92c7a9ea76df"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
|
||||||
integrity sha512-bUTfqFWtNKWp73oNIfRkqwYZJeNT3lstzZcAkhhiuvDraRSgOH1/+F9ZklbpR4zpdKuo4cpXN8tKP7s61yjX+g==
|
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
core-util-is@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@@ -254,11 +253,6 @@ is-arrayish@^0.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||||
|
|
||||||
is-buffer@^2.0.2:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
|
|
||||||
integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
|
|
||||||
|
|
||||||
is-stream@^1.1.0:
|
is-stream@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||||
@@ -466,9 +460,9 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
|||||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||||
|
|
||||||
uuid@^3.3.2:
|
uuid@^3.3.2:
|
||||||
version "3.3.3"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
winston-transport@^4.3.0:
|
winston-transport@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-plugin-manager",
|
"name": "terminus-plugin-manager",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "Terminus' plugin manager",
|
"description": "Terminus' plugin manager",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"@types/semver": "^6.0.0",
|
"@types/semver": "^6.0.0",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"mz": "^2.6.0",
|
"mz": "^2.6.0",
|
||||||
"semver": "^6.1.0"
|
"semver": "^7.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^7",
|
"@angular/common": "^7",
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
.d-flex
|
.d-flex
|
||||||
h3.mb-1 Installed
|
h3.mb-1 Installed
|
||||||
button.btn.btn-outline-info.btn-sm.ml-auto((click)='openPluginsFolder()')
|
button.btn.btn-outline-secondary.btn-sm.ml-auto((click)='openPluginsFolder()')
|
||||||
i.fas.fa-folder
|
i.fas.fa-folder
|
||||||
span Plugins folder
|
span Plugins folder
|
||||||
|
|
||||||
@@ -28,19 +28,19 @@
|
|||||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
|
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
|
||||||
span Upgrade ({{knownUpgrades[plugin.name].version}})
|
span Upgrade ({{knownUpgrades[plugin.name].version}})
|
||||||
|
|
||||||
button.btn.btn-primary.ml-2(
|
button.btn.btn-link.text-primary.ml-2(
|
||||||
*ngIf='config.store.pluginBlacklist.includes(plugin.name)',
|
*ngIf='config.store.pluginBlacklist.includes(plugin.name)',
|
||||||
(click)='enablePlugin(plugin)'
|
(click)='enablePlugin(plugin)'
|
||||||
)
|
)
|
||||||
i.fas.fa-fw.fa-play
|
i.fas.fa-fw.fa-play
|
||||||
|
|
||||||
button.btn.btn-secondary.ml-2(
|
button.btn.btn-link.ml-2(
|
||||||
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
|
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
|
||||||
(click)='disablePlugin(plugin)'
|
(click)='disablePlugin(plugin)'
|
||||||
)
|
)
|
||||||
i.fas.fa-fw.fa-pause
|
i.fas.fa-fw.fa-pause
|
||||||
|
|
||||||
button.btn.btn-danger.ml-2(
|
button.btn.btn-link.text-danger.ml-2(
|
||||||
(click)='uninstallPlugin(plugin)',
|
(click)='uninstallPlugin(plugin)',
|
||||||
*ngIf='!plugin.isBuiltin',
|
*ngIf='!plugin.isBuiltin',
|
||||||
[disabled]='busy[plugin.name] != undefined'
|
[disabled]='busy[plugin.name] != undefined'
|
||||||
|
@@ -8,6 +8,10 @@ const NAME_PREFIX = 'terminus-'
|
|||||||
const KEYWORD = 'terminus-plugin'
|
const KEYWORD = 'terminus-plugin'
|
||||||
const OFFICIAL_NPM_ACCOUNT = 'eugenepankov'
|
const OFFICIAL_NPM_ACCOUNT = 'eugenepankov'
|
||||||
|
|
||||||
|
const BLACKLIST = [
|
||||||
|
'terminus-shell-selector', // superseded by profiles
|
||||||
|
]
|
||||||
|
|
||||||
export interface PluginInfo {
|
export interface PluginInfo {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -75,6 +79,7 @@ export class PluginManagerService {
|
|||||||
isOfficial: item.package.publisher.username === OFFICIAL_NPM_ACCOUNT,
|
isOfficial: item.package.publisher.username === OFFICIAL_NPM_ACCOUNT,
|
||||||
}))),
|
}))),
|
||||||
map(plugins => plugins.filter(x => x.packageName.startsWith(NAME_PREFIX))),
|
map(plugins => plugins.filter(x => x.packageName.startsWith(NAME_PREFIX))),
|
||||||
|
map(plugins => plugins.filter(x => !BLACKLIST.includes(x.packageName))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,12 +13,11 @@ any-promise@^1.0.0:
|
|||||||
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
|
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
|
||||||
|
|
||||||
axios@^0.19.0:
|
axios@^0.19.0:
|
||||||
version "0.19.0"
|
version "0.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||||
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
|
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "1.5.10"
|
follow-redirects "1.5.10"
|
||||||
is-buffer "^2.0.2"
|
|
||||||
|
|
||||||
debug@=3.1.0:
|
debug@=3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
@@ -34,11 +33,6 @@ follow-redirects@1.5.10:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "=3.1.0"
|
debug "=3.1.0"
|
||||||
|
|
||||||
is-buffer@^2.0.2:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
|
|
||||||
integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
|
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
@@ -58,10 +52,10 @@ object-assign@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||||
|
|
||||||
semver@^6.1.0:
|
semver@^7.1.1:
|
||||||
version "6.3.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667"
|
||||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A==
|
||||||
|
|
||||||
thenify-all@^1.0.0:
|
thenify-all@^1.0.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-settings",
|
"name": "terminus-settings",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "Terminus terminal settings page",
|
"description": "Terminus terminal settings page",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import * as os from 'os'
|
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { Component, Inject, Input, HostBinding, NgZone } from '@angular/core'
|
import { Component, Inject, Input, HostBinding, NgZone } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +13,8 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
HomeBaseService,
|
HomeBaseService,
|
||||||
ShellIntegrationService,
|
ShellIntegrationService,
|
||||||
|
isWindowsBuild,
|
||||||
|
WIN_BUILD_FLUENT_BG_SUPPORTED,
|
||||||
} from 'terminus-core'
|
} from 'terminus-core'
|
||||||
|
|
||||||
import { SettingsTabProvider } from '../api'
|
import { SettingsTabProvider } from '../api'
|
||||||
@@ -81,9 +82,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
|||||||
this.hotkeyDescriptions = descriptions
|
this.hotkeyDescriptions = descriptions
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isFluentVibrancySupported = hostApp.platform === Platform.Windows
|
this.isFluentVibrancySupported = isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)
|
||||||
&& parseFloat(os.release()) >= 10
|
|
||||||
&& parseInt(os.release().split('.')[2]) >= 17063
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
@@ -99,10 +98,6 @@ export class SettingsTabComponent extends BaseTabComponent {
|
|||||||
this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
|
this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecoveryToken (): Promise<any> {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.configSubscription.unsubscribe()
|
this.configSubscription.unsubscribe()
|
||||||
this.config.save()
|
this.config.save()
|
||||||
|
@@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'
|
|||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
import TerminusCorePlugin, { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
|
import TerminusCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
|
||||||
|
|
||||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||||
@@ -11,7 +11,6 @@ import { SettingsTabComponent } from './components/settingsTab.component'
|
|||||||
import { SettingsTabBodyComponent } from './components/settingsTabBody.component'
|
import { SettingsTabBodyComponent } from './components/settingsTabBody.component'
|
||||||
|
|
||||||
import { ButtonProvider } from './buttonProvider'
|
import { ButtonProvider } from './buttonProvider'
|
||||||
import { RecoveryProvider } from './recoveryProvider'
|
|
||||||
import { SettingsHotkeyProvider } from './hotkeys'
|
import { SettingsHotkeyProvider } from './hotkeys'
|
||||||
import { SettingsConfigProvider } from './config'
|
import { SettingsConfigProvider } from './config'
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ import { SettingsConfigProvider } from './config'
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
|
||||||
{ provide: ConfigProvider, useClass: SettingsConfigProvider, multi: true },
|
{ provide: ConfigProvider, useClass: SettingsConfigProvider, multi: true },
|
||||||
{ provide: HotkeyProvider, useClass: SettingsHotkeyProvider, multi: true },
|
{ provide: HotkeyProvider, useClass: SettingsHotkeyProvider, multi: true },
|
||||||
],
|
],
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
|
||||||
|
|
||||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
|
||||||
|
|
||||||
/** @hidden */
|
|
||||||
@Injectable()
|
|
||||||
export class RecoveryProvider extends TabRecoveryProvider {
|
|
||||||
async recover (recoveryToken: any): Promise<RecoveredTab|null> {
|
|
||||||
if (recoveryToken && recoveryToken.type === 'app:settings') {
|
|
||||||
return { type: SettingsTabComponent }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-ssh",
|
"name": "terminus-ssh",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "SSH connection manager for Terminus",
|
"description": "SSH connection manager for Terminus",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
@@ -19,8 +19,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "12.7.3",
|
"@types/node": "12.7.3",
|
||||||
"@types/ssh2": "^0.5.35",
|
"@types/ssh2": "^0.5.35",
|
||||||
|
"ansi-colors": "^4.1.1",
|
||||||
|
"cli-spinner": "^0.2.10",
|
||||||
"ssh2": "^0.8.2",
|
"ssh2": "^0.8.2",
|
||||||
"ssh2-streams": "^0.4.2"
|
"ssh2-streams": "^0.4.2",
|
||||||
|
"sshpk": "^1.16.1",
|
||||||
|
"terminus-terminal": "^1.0.98-nightly.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^7",
|
"@angular/common": "^7",
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
|
import colors from 'ansi-colors'
|
||||||
import { BaseSession } from 'terminus-terminal'
|
import { BaseSession } from 'terminus-terminal'
|
||||||
|
import { Server, Socket, createServer, createConnection } from 'net'
|
||||||
|
import { Client, ClientChannel } from 'ssh2'
|
||||||
|
import { Logger } from 'terminus-core'
|
||||||
|
import { Subject, Observable } from 'rxjs'
|
||||||
|
|
||||||
export interface LoginScript {
|
export interface LoginScript {
|
||||||
expect: string
|
expect: string
|
||||||
@@ -21,30 +26,88 @@ export interface SSHConnection {
|
|||||||
user: string
|
user: string
|
||||||
password?: string
|
password?: string
|
||||||
privateKey?: string
|
privateKey?: string
|
||||||
group?: string
|
group: string | null
|
||||||
scripts?: LoginScript[]
|
scripts?: LoginScript[]
|
||||||
keepaliveInterval?: number
|
keepaliveInterval?: number
|
||||||
keepaliveCountMax?: number
|
keepaliveCountMax?: number
|
||||||
readyTimeout?: number
|
readyTimeout?: number
|
||||||
|
color?: string
|
||||||
|
x11?: boolean
|
||||||
|
skipBanner?: boolean
|
||||||
|
|
||||||
algorithms?: {[t: string]: string[]}
|
algorithms?: {[t: string]: string[]}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PortForwardType {
|
||||||
|
Local, Remote
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForwardedPort {
|
||||||
|
type: PortForwardType
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port: number
|
||||||
|
targetAddress: string
|
||||||
|
targetPort: number
|
||||||
|
|
||||||
|
private listener: Server
|
||||||
|
|
||||||
|
async startLocalListener (callback: (Socket) => void): Promise<void> {
|
||||||
|
this.listener = createServer(callback)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.listener.listen(this.port, '127.0.0.1')
|
||||||
|
this.listener.on('error', reject)
|
||||||
|
this.listener.on('listening', resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLocalListener () {
|
||||||
|
this.listener.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
toString () {
|
||||||
|
if (this.type === PortForwardType.Local) {
|
||||||
|
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
|
||||||
|
} else {
|
||||||
|
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SSHSession extends BaseSession {
|
export class SSHSession extends BaseSession {
|
||||||
scripts?: LoginScript[]
|
scripts?: LoginScript[]
|
||||||
shell: any
|
shell: ClientChannel
|
||||||
|
ssh: Client
|
||||||
|
forwardedPorts: ForwardedPort[] = []
|
||||||
|
logger: Logger
|
||||||
|
|
||||||
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||||
|
private serviceMessage = new Subject<string>()
|
||||||
|
|
||||||
constructor (public connection: SSHConnection) {
|
constructor (public connection: SSHConnection) {
|
||||||
super()
|
super()
|
||||||
this.scripts = connection.scripts || []
|
this.scripts = connection.scripts || []
|
||||||
}
|
}
|
||||||
|
|
||||||
start () {
|
async start () {
|
||||||
this.open = true
|
this.open = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.shell = await this.openShellChannel({ x11: this.connection.x11 })
|
||||||
|
} catch (err) {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shell.on('greeting', greeting => {
|
||||||
|
this.emitServiceMessage(`Shell greeting: ${greeting}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.shell.on('banner', banner => {
|
||||||
|
this.emitServiceMessage(`Shell banner: ${banner}`)
|
||||||
|
})
|
||||||
|
|
||||||
this.shell.on('data', data => {
|
this.shell.on('data', data => {
|
||||||
const dataString = data.toString()
|
const dataString = data.toString()
|
||||||
this.emitOutput(dataString)
|
this.emitOutput(data)
|
||||||
|
|
||||||
if (this.scripts) {
|
if (this.scripts) {
|
||||||
let found = false
|
let found = false
|
||||||
@@ -67,12 +130,12 @@ export class SSHSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log('Executing script: "' + cmd + '"')
|
this.logger.info('Executing script: "' + cmd + '"')
|
||||||
this.shell.write(cmd + '\n')
|
this.shell.write(cmd + '\n')
|
||||||
this.scripts = this.scripts.filter(x => x !== script)
|
this.scripts = this.scripts.filter(x => x !== script)
|
||||||
} else {
|
} else {
|
||||||
if (script.optional) {
|
if (script.optional) {
|
||||||
console.log('Skip optional script: ' + script.expect)
|
this.logger.debug('Skip optional script: ' + script.expect)
|
||||||
found = true
|
found = true
|
||||||
this.scripts = this.scripts.filter(x => x !== script)
|
this.scripts = this.scripts.filter(x => x !== script)
|
||||||
} else {
|
} else {
|
||||||
@@ -88,17 +151,140 @@ export class SSHSession extends BaseSession {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.shell.on('end', () => {
|
this.shell.on('end', () => {
|
||||||
|
this.logger.info('Shell session ended')
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.destroy()
|
this.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.ssh.on('tcp connection', (details, accept, reject) => {
|
||||||
|
this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
|
||||||
|
const forward = this.forwardedPorts.find(x => x.port === details.destPort)
|
||||||
|
if (!forward) {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
||||||
|
return reject()
|
||||||
|
}
|
||||||
|
const socket = new Socket()
|
||||||
|
socket.connect(forward.targetPort, forward.targetAddress)
|
||||||
|
socket.on('error', e => {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
socket.on('connect', () => {
|
||||||
|
this.logger.info('Connection forwarded')
|
||||||
|
const stream = accept()
|
||||||
|
stream.pipe(socket)
|
||||||
|
socket.pipe(stream)
|
||||||
|
stream.on('close', () => {
|
||||||
|
socket.destroy()
|
||||||
|
})
|
||||||
|
socket.on('close', () => {
|
||||||
|
stream.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ssh.on('x11', (details, accept, reject) => {
|
||||||
|
this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
|
||||||
|
let displaySpec = process.env.DISPLAY || ':0'
|
||||||
|
this.logger.debug(`Trying display ${displaySpec}`)
|
||||||
|
let xHost = displaySpec.split(':')[0]
|
||||||
|
let xDisplay = parseInt(displaySpec.split(':')[1].split('.')[0] || '0')
|
||||||
|
let xPort = xDisplay < 100 ? xDisplay + 6000 : xDisplay
|
||||||
|
|
||||||
|
const socket = displaySpec.startsWith('/') ? createConnection(displaySpec) : new Socket()
|
||||||
|
if (!displaySpec.startsWith('/')) {
|
||||||
|
socket.connect(xPort, xHost)
|
||||||
|
}
|
||||||
|
socket.on('error', e => {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server ${xHost}:${xPort}: ${e}`)
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
socket.on('connect', () => {
|
||||||
|
this.logger.info('Connection forwarded')
|
||||||
|
const stream = accept()
|
||||||
|
stream.pipe(socket)
|
||||||
|
socket.pipe(stream)
|
||||||
|
stream.on('close', () => {
|
||||||
|
socket.destroy()
|
||||||
|
})
|
||||||
|
socket.on('close', () => {
|
||||||
|
stream.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
this.executeUnconditionalScripts()
|
this.executeUnconditionalScripts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitServiceMessage (msg: string) {
|
||||||
|
this.serviceMessage.next(msg)
|
||||||
|
this.logger.info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async addPortForward (fw: ForwardedPort) {
|
||||||
|
if (fw.type === PortForwardType.Local) {
|
||||||
|
await fw.startLocalListener((socket: Socket) => {
|
||||||
|
this.logger.info(`New connection on ${fw}`)
|
||||||
|
this.ssh.forwardOut(
|
||||||
|
socket.remoteAddress || '127.0.0.1',
|
||||||
|
socket.remotePort || 0,
|
||||||
|
fw.targetAddress,
|
||||||
|
fw.targetPort,
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwaded connection via ${fw}: ${err}`)
|
||||||
|
socket.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream.pipe(socket)
|
||||||
|
socket.pipe(stream)
|
||||||
|
stream.on('close', () => {
|
||||||
|
socket.destroy()
|
||||||
|
})
|
||||||
|
socket.on('close', () => {
|
||||||
|
stream.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}).then(() => {
|
||||||
|
this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwaded ${fw}`)
|
||||||
|
this.forwardedPorts.push(fw)
|
||||||
|
}).catch(e => {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (fw.type === PortForwardType.Remote) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
this.ssh.forwardIn(fw.host, fw.port, err => {
|
||||||
|
if (err) {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwaded ${fw}`)
|
||||||
|
this.forwardedPorts.push(fw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePortForward (fw: ForwardedPort) {
|
||||||
|
if (fw.type === PortForwardType.Local) {
|
||||||
|
fw.stopLocalListener()
|
||||||
|
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
|
}
|
||||||
|
if (fw.type === PortForwardType.Remote) {
|
||||||
|
this.ssh.unforwardIn(fw.host, fw.port)
|
||||||
|
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
|
}
|
||||||
|
this.emitServiceMessage(`Stopped forwarding ${fw}`)
|
||||||
|
}
|
||||||
|
|
||||||
resize (columns, rows) {
|
resize (columns, rows) {
|
||||||
if (this.shell) {
|
if (this.shell) {
|
||||||
this.shell.setWindow(rows, columns)
|
this.shell.setWindow(rows, columns, rows, columns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +300,11 @@ export class SSHSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async destroy (): Promise<void> {
|
||||||
|
this.serviceMessage.complete()
|
||||||
|
await super.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
async getChildProcesses (): Promise<any[]> {
|
async getChildProcesses (): Promise<any[]> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -126,6 +317,18 @@ export class SSHSession extends BaseSession {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openShellChannel (options): Promise<ClientChannel> {
|
||||||
|
return new Promise<ClientChannel>((resolve, reject) => {
|
||||||
|
this.ssh.shell({ term: 'xterm-256color' }, options, (err, shell) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(shell)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private executeUnconditionalScripts () {
|
private executeUnconditionalScripts () {
|
||||||
if (this.scripts) {
|
if (this.scripts) {
|
||||||
for (const script of this.scripts) {
|
for (const script of this.scripts) {
|
||||||
|
@@ -1,118 +1,152 @@
|
|||||||
.modal-body
|
.modal-body
|
||||||
ngb-tabset(type='pills', [activeId]='basic')
|
ngb-tabset([activeId]='basic')
|
||||||
ngb-tab(id='basic')
|
ngb-tab(id='basic')
|
||||||
ng-template(ngbTabTitle) General
|
ng-template(ngbTabTitle) General
|
||||||
ng-template(ngbTabContent)
|
ng-template(ngbTabContent)
|
||||||
.form-group
|
.form-group
|
||||||
label Name
|
label Name
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
autofocus,
|
autofocus,
|
||||||
[(ngModel)]='connection.name',
|
[(ngModel)]='connection.name',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Group
|
label Group
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
placeholder='Ungrouped',
|
placeholder='Ungrouped',
|
||||||
[(ngModel)]='connection.group',
|
[(ngModel)]='connection.group',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Host
|
label Host
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='connection.host',
|
[(ngModel)]='connection.host',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Port
|
label Port
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='number',
|
type='number',
|
||||||
placeholder='22',
|
placeholder='22',
|
||||||
[(ngModel)]='connection.port',
|
[(ngModel)]='connection.port',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Username
|
label Username
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='connection.user',
|
[(ngModel)]='connection.user',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
.title Password
|
.title Password
|
||||||
.description(*ngIf='!hasSavedPassword') Save a password in the keychain
|
.description(*ngIf='!hasSavedPassword') Save a password in the keychain
|
||||||
.description(*ngIf='hasSavedPassword') There is a saved password for this connection
|
.description(*ngIf='hasSavedPassword') There is a saved password for this connection
|
||||||
button.btn.btn-outline-success.ml-4(*ngIf='!hasSavedPassword', (click)='setPassword()')
|
button.btn.btn-outline-success.ml-4(*ngIf='!hasSavedPassword', (click)='setPassword()')
|
||||||
i.fas.fa-key
|
i.fas.fa-key
|
||||||
span Set password
|
span Set password
|
||||||
button.btn.btn-danger.ml-4(*ngIf='hasSavedPassword', (click)='clearSavedPassword()')
|
button.btn.btn-danger.ml-4(*ngIf='hasSavedPassword', (click)='clearSavedPassword()')
|
||||||
i.fas.fa-trash-alt
|
i.fas.fa-trash-alt
|
||||||
span Forget
|
span Forget
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
.title Private key
|
.title Private key
|
||||||
.description Path to the private key file
|
.description Path to the private key file
|
||||||
.input-group
|
.input-group
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
placeholder='Key file path',
|
placeholder='Key file path',
|
||||||
[(ngModel)]='connection.privateKey'
|
[(ngModel)]='connection.privateKey'
|
||||||
)
|
)
|
||||||
.input-group-btn
|
.input-group-append
|
||||||
button.btn.btn-secondary((click)='selectPrivateKey()')
|
button.btn.btn-secondary((click)='selectPrivateKey()')
|
||||||
i.fas.fa-folder-open
|
i.fas.fa-folder-open
|
||||||
|
|
||||||
ngb-tab(id='advanced')
|
ngb-tab(id='advanced')
|
||||||
ng-template(ngbTabTitle) Advanced
|
ng-template(ngbTabTitle) Advanced
|
||||||
ng-template(ngbTabContent)
|
ng-template(ngbTabContent)
|
||||||
.form-group
|
.form-line
|
||||||
label Keep Alive Interval (Milliseconds)
|
.header
|
||||||
|
.title X11 forwarding
|
||||||
|
toggle([(ngModel)]='connection.x11')
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Tab color
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='number',
|
type='text',
|
||||||
placeholder='0',
|
autofocus,
|
||||||
[(ngModel)]='connection.keepaliveInterval',
|
[(ngModel)]='connection.color',
|
||||||
|
placeholder='#000000'
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-line
|
||||||
label Max Keep Alive Count
|
.header
|
||||||
|
.title Skip MoTD/banner
|
||||||
|
toggle([(ngModel)]='connection.skipBanner')
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Keep Alive Interval (Milliseconds)
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='number',
|
type='number',
|
||||||
placeholder='3',
|
placeholder='0',
|
||||||
[(ngModel)]='connection.keepaliveCountMax',
|
[(ngModel)]='connection.keepaliveInterval',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-line
|
||||||
label Ready Timeout (Milliseconds)
|
.header
|
||||||
|
.title Max Keep Alive Count
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='number',
|
type='number',
|
||||||
placeholder='20000',
|
placeholder='3',
|
||||||
[(ngModel)]='connection.readyTimeout',
|
[(ngModel)]='connection.keepaliveCountMax',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
|
||||||
label Ciphers
|
|
||||||
div(*ngFor='let alg of supportedAlgorithms.cipher')
|
|
||||||
checkbox([text]='alg', [(ngModel)]='algorithms.cipher[alg]')
|
|
||||||
|
|
||||||
.form-group
|
.form-line
|
||||||
label Key exchange
|
.header
|
||||||
div(*ngFor='let alg of supportedAlgorithms.kex')
|
.title Ready Timeout (Milliseconds)
|
||||||
checkbox([text]='alg', [(ngModel)]='algorithms.kex[alg]')
|
input.form-control(
|
||||||
|
type='number',
|
||||||
|
placeholder='20000',
|
||||||
|
[(ngModel)]='connection.readyTimeout',
|
||||||
|
)
|
||||||
|
|
||||||
.form-group
|
ngb-tab(id='ciphers')
|
||||||
label HMAC
|
ng-template(ngbTabTitle) Ciphers
|
||||||
div(*ngFor='let alg of supportedAlgorithms.hmac')
|
ng-template(ngbTabContent)
|
||||||
checkbox([text]='alg', [(ngModel)]='algorithms.hmac[alg]')
|
.form-line.align-items-start
|
||||||
|
.header
|
||||||
|
.title Ciphers
|
||||||
|
.w-50
|
||||||
|
div(*ngFor='let alg of supportedAlgorithms.cipher')
|
||||||
|
checkbox([text]='alg', [(ngModel)]='algorithms.cipher[alg]')
|
||||||
|
|
||||||
.form-group
|
.form-line.align-items-start
|
||||||
label Host key
|
.header
|
||||||
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
|
.title Key exchange
|
||||||
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
|
.w-50
|
||||||
|
div(*ngFor='let alg of supportedAlgorithms.kex')
|
||||||
|
checkbox([text]='alg', [(ngModel)]='algorithms.kex[alg]')
|
||||||
|
|
||||||
|
.form-line.align-items-start
|
||||||
|
.header
|
||||||
|
.title HMAC
|
||||||
|
.w-50
|
||||||
|
div(*ngFor='let alg of supportedAlgorithms.hmac')
|
||||||
|
checkbox([text]='alg', [(ngModel)]='algorithms.hmac[alg]')
|
||||||
|
|
||||||
|
.form-line.align-items-start
|
||||||
|
.header
|
||||||
|
.title Host key
|
||||||
|
.w-50
|
||||||
|
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
|
||||||
|
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
|
||||||
|
|
||||||
|
|
||||||
ngb-tab(id='scripts')
|
ngb-tab(id='scripts')
|
||||||
@@ -128,12 +162,12 @@
|
|||||||
tr(*ngFor='let script of connection.scripts')
|
tr(*ngFor='let script of connection.scripts')
|
||||||
td.pr-2
|
td.pr-2
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='script.expect'
|
[(ngModel)]='script.expect'
|
||||||
)
|
)
|
||||||
td
|
td
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='script.send'
|
[(ngModel)]='script.send'
|
||||||
)
|
)
|
||||||
td.pl-2
|
td.pl-2
|
||||||
@@ -152,11 +186,11 @@
|
|||||||
i.fas.fa-arrow-down
|
i.fas.fa-arrow-down
|
||||||
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
|
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
|
||||||
i.fas.fa-trash
|
i.fas.fa-trash
|
||||||
|
|
||||||
button.btn.btn-outline-info.mt-2((click)='addScript()')
|
button.btn.btn-outline-info.mt-2((click)='addScript()')
|
||||||
i.fas.fa-plus
|
i.fas.fa-plus
|
||||||
span New item
|
span New item
|
||||||
|
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-outline-primary((click)='save()') Save
|
button.btn.btn-outline-primary((click)='save()') Save
|
||||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
||||||
|
@@ -66,7 +66,7 @@ export class EditConnectionModalComponent {
|
|||||||
modal.componentInstance.password = true
|
modal.componentInstance.password = true
|
||||||
try {
|
try {
|
||||||
const result = await modal.result
|
const result = await modal.result
|
||||||
if (result && result.value) {
|
if (result?.value) {
|
||||||
this.passwordStorage.savePassword(this.connection, result.value)
|
this.passwordStorage.savePassword(this.connection, result.value)
|
||||||
this.hasSavedPassword = true
|
this.hasSavedPassword = true
|
||||||
}
|
}
|
||||||
@@ -82,10 +82,11 @@ export class EditConnectionModalComponent {
|
|||||||
this.electron.dialog.showOpenDialog(
|
this.electron.dialog.showOpenDialog(
|
||||||
this.hostApp.getWindow(),
|
this.hostApp.getWindow(),
|
||||||
{
|
{
|
||||||
|
defaultPath: this.connection.privateKey,
|
||||||
title: 'Select private key',
|
title: 'Select private key',
|
||||||
}
|
}
|
||||||
).then(result => {
|
).then(result => {
|
||||||
if (result.filePaths) {
|
if (!result.canceled) {
|
||||||
this.connection.privateKey = result.filePaths[0]
|
this.connection.privateKey = result.filePaths[0]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -49,6 +49,7 @@ export class SSHModalComponent {
|
|||||||
|
|
||||||
const connection: SSHConnection = {
|
const connection: SSHConnection = {
|
||||||
name: this.quickTarget,
|
name: this.quickTarget,
|
||||||
|
group: null,
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
port,
|
port,
|
||||||
@@ -91,7 +92,7 @@ export class SSHModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const connection of connections) {
|
for (const connection of connections) {
|
||||||
connection.group = connection.group || undefined
|
connection.group = connection.group || null
|
||||||
let group = this.childGroups.find(x => x.name === connection.group)
|
let group = this.childGroups.find(x => x.name === connection.group)
|
||||||
if (!group) {
|
if (!group) {
|
||||||
group = {
|
group = {
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
.modal-header
|
||||||
|
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
|
||||||
|
.ml-3 {{fw.host}}:{{fw.port}} → {{fw.targetAddress}}:{{fw.targetPort}}
|
||||||
|
button.btn.btn-link.ml-auto((click)='remove(fw)')
|
||||||
|
i.fas.fa-trash-alt.mr-2
|
||||||
|
span Remove
|
||||||
|
|
||||||
|
.input-group.mb-2
|
||||||
|
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
|
||||||
|
|
||||||
|
button.btn.btn-primary((click)='addForward()')
|
||||||
|
i.fas.fa-check.mr-2
|
||||||
|
span Forward port
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ForwardedPort, PortForwardType, 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
|
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
|
||||||
|
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||||
import { SSHConnection, SSHConnectionGroup } from '../api'
|
import { SSHConnection, SSHConnectionGroup } from '../api'
|
||||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||||
import { PromptModalComponent } from './promptModal.component'
|
import { PromptModalComponent } from './promptModal.component'
|
||||||
@@ -19,6 +20,7 @@ export class SSHSettingsTabComponent {
|
|||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
|
private passwordStorage: PasswordStorageService,
|
||||||
) {
|
) {
|
||||||
this.connections = this.config.store.ssh.connections
|
this.connections = this.config.store.ssh.connections
|
||||||
this.refresh()
|
this.refresh()
|
||||||
@@ -27,6 +29,7 @@ export class SSHSettingsTabComponent {
|
|||||||
createConnection () {
|
createConnection () {
|
||||||
const connection: SSHConnection = {
|
const connection: SSHConnection = {
|
||||||
name: '',
|
name: '',
|
||||||
|
group: null,
|
||||||
host: '',
|
host: '',
|
||||||
port: 22,
|
port: 22,
|
||||||
user: 'root',
|
user: 'root',
|
||||||
@@ -64,6 +67,7 @@ export class SSHSettingsTabComponent {
|
|||||||
}
|
}
|
||||||
)).response === 1) {
|
)).response === 1) {
|
||||||
this.connections = this.connections.filter(x => x !== connection)
|
this.connections = this.connections.filter(x => x !== connection)
|
||||||
|
this.passwordStorage.deletePassword(connection)
|
||||||
this.config.store.ssh.connections = this.connections
|
this.config.store.ssh.connections = this.connections
|
||||||
this.config.save()
|
this.config.save()
|
||||||
this.refresh()
|
this.refresh()
|
||||||
@@ -97,7 +101,7 @@ export class SSHSettingsTabComponent {
|
|||||||
}
|
}
|
||||||
)).response === 1) {
|
)).response === 1) {
|
||||||
for (const connection of this.connections.filter(x => x.group === group.name)) {
|
for (const connection of this.connections.filter(x => x.group === group.name)) {
|
||||||
connection.group = undefined
|
connection.group = null
|
||||||
}
|
}
|
||||||
this.config.save()
|
this.config.save()
|
||||||
this.refresh()
|
this.refresh()
|
||||||
@@ -109,7 +113,7 @@ export class SSHSettingsTabComponent {
|
|||||||
this.childGroups = []
|
this.childGroups = []
|
||||||
|
|
||||||
for (const connection of this.connections) {
|
for (const connection of this.connections) {
|
||||||
connection.group = connection.group || undefined
|
connection.group = connection.group || null
|
||||||
let group = this.childGroups.find(x => x.name === connection.group)
|
let group = this.childGroups.find(x => x.name === connection.group)
|
||||||
if (!group) {
|
if (!group) {
|
||||||
group = {
|
group = {
|
||||||
|
15
terminus-ssh/src/components/sshTab.component.pug
Normal file
15
terminus-ssh/src/components/sshTab.component.pug
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.ssh-tab-toolbar
|
||||||
|
.btn.btn-outline-secondary.reveal-button
|
||||||
|
i.fas.fa-ellipsis-h
|
||||||
|
.toolbar(*ngIf='session', [class.show]='!session.open')
|
||||||
|
i.fas.fa-circle.text-success.mr-2(*ngIf='session.open')
|
||||||
|
i.fas.fa-circle.text-danger.mr-2(*ngIf='!session.open')
|
||||||
|
strong.mr-auto(*ngIf='session') {{session.connection.user}}@{{session.connection.host}}:{{session.connection.port}}
|
||||||
|
|
||||||
|
button.btn.btn-secondary((click)='showPortForwarding()', *ngIf='session.open')
|
||||||
|
i.fas.fa-plug
|
||||||
|
span Ports
|
||||||
|
|
||||||
|
button.btn.btn-info((click)='reconnect()', *ngIf='!session.open')
|
||||||
|
i.fas.fa-reload
|
||||||
|
span Reconnect
|
@@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&> .content {
|
&> .content {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
@@ -11,4 +12,60 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ssh-tab-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 4;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.reveal-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 35px;
|
||||||
|
padding: 0;
|
||||||
|
height: 35px;
|
||||||
|
line-height: 35px;
|
||||||
|
transition: 0.125s opacity;
|
||||||
|
opacity: .5;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .reveal-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .toolbar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
opacity: 0;
|
||||||
|
background: rgba(0, 0, 0, .75);
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: 0.25s opacity;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
.reveal-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
|
import colors from 'ansi-colors'
|
||||||
|
import { Spinner } from 'cli-spinner'
|
||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { BaseTerminalTabComponent } from 'terminus-terminal'
|
import { BaseTerminalTabComponent } from 'terminus-terminal'
|
||||||
import { SSHService } from '../services/ssh.service'
|
import { SSHService } from '../services/ssh.service'
|
||||||
import { SSHConnection, SSHSession } from '../api'
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
|
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: BaseTerminalTabComponent.template,
|
selector: 'ssh-tab',
|
||||||
|
template: BaseTerminalTabComponent.template + require<string>('./sshTab.component.pug'),
|
||||||
styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles],
|
styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles],
|
||||||
animations: BaseTerminalTabComponent.animations,
|
animations: BaseTerminalTabComponent.animations,
|
||||||
})
|
})
|
||||||
@@ -14,8 +19,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
connection: SSHConnection
|
connection: SSHConnection
|
||||||
ssh: SSHService
|
ssh: SSHService
|
||||||
session: SSHSession
|
session: SSHSession
|
||||||
|
private ngbModal: NgbModal
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
this.ngbModal = this.injector.get<NgbModal>(NgbModal)
|
||||||
|
|
||||||
this.logger = this.log.create('terminalTab')
|
this.logger = this.log.create('terminalTab')
|
||||||
this.ssh = this.injector.get(SSHService)
|
this.ssh = this.injector.get(SSHService)
|
||||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||||
@@ -35,30 +43,53 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session = new SSHSession(this.connection)
|
this.session = this.ssh.createSession(this.connection)
|
||||||
|
this.session.serviceMessage$.subscribe(msg => {
|
||||||
|
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ' ' + msg + '\r\n')
|
||||||
|
this.session.resize(this.size.columns, this.size.rows)
|
||||||
|
})
|
||||||
this.attachSessionHandlers()
|
this.attachSessionHandlers()
|
||||||
this.write(`Connecting to ${this.connection.host}`)
|
this.write(`Connecting to ${this.connection.host}`)
|
||||||
const interval = setInterval(() => this.write('.'), 500)
|
|
||||||
|
const spinner = new Spinner({
|
||||||
|
text: 'Connecting',
|
||||||
|
stream: {
|
||||||
|
write: x => this.write(x),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
spinner.setSpinnerString(6)
|
||||||
|
spinner.start()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ssh.connectSession(this.session, (message: string) => {
|
await this.ssh.connectSession(this.session, (message: string) => {
|
||||||
this.write('\r\n' + message)
|
spinner.stop(true)
|
||||||
|
this.write(message + '\r\n')
|
||||||
|
spinner.start()
|
||||||
})
|
})
|
||||||
|
spinner.stop(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.write('\r\n')
|
spinner.stop(true)
|
||||||
this.write(e.message)
|
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||||
return
|
return
|
||||||
} finally {
|
|
||||||
clearInterval(interval)
|
|
||||||
this.write('\r\n')
|
|
||||||
}
|
}
|
||||||
|
await this.session.start()
|
||||||
this.session.resize(this.size.columns, this.size.rows)
|
this.session.resize(this.size.columns, this.size.rows)
|
||||||
this.session.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecoveryToken (): Promise<any> {
|
async getRecoveryToken (): Promise<any> {
|
||||||
return {
|
return {
|
||||||
type: 'app:ssh-tab',
|
type: 'app:ssh-tab',
|
||||||
connection: this.connection,
|
connection: this.connection,
|
||||||
|
savedState: this.frontend?.saveState(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showPortForwarding () {
|
||||||
|
const modal = this.ngbModal.open(SSHPortForwardingModalComponent).componentInstance as SSHPortForwardingModalComponent
|
||||||
|
modal.session = this.session
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect () {
|
||||||
|
this.initializeSession()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import TerminusTerminalModule from 'terminus-terminal'
|
|||||||
|
|
||||||
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
||||||
import { SSHModalComponent } from './components/sshModal.component'
|
import { SSHModalComponent } from './components/sshModal.component'
|
||||||
|
import { SSHPortForwardingModalComponent } from './components/sshPortForwardingModal.component'
|
||||||
import { PromptModalComponent } from './components/promptModal.component'
|
import { PromptModalComponent } from './components/promptModal.component'
|
||||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||||
import { SSHTabComponent } from './components/sshTab.component'
|
import { SSHTabComponent } from './components/sshTab.component'
|
||||||
@@ -40,6 +41,7 @@ import { SSHHotkeyProvider } from './hotkeys'
|
|||||||
EditConnectionModalComponent,
|
EditConnectionModalComponent,
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
SSHModalComponent,
|
SSHModalComponent,
|
||||||
|
SSHPortForwardingModalComponent,
|
||||||
SSHSettingsTabComponent,
|
SSHSettingsTabComponent,
|
||||||
SSHTabComponent,
|
SSHTabComponent,
|
||||||
],
|
],
|
||||||
@@ -47,6 +49,7 @@ import { SSHHotkeyProvider } from './hotkeys'
|
|||||||
EditConnectionModalComponent,
|
EditConnectionModalComponent,
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
SSHModalComponent,
|
SSHModalComponent,
|
||||||
|
SSHPortForwardingModalComponent,
|
||||||
SSHSettingsTabComponent,
|
SSHSettingsTabComponent,
|
||||||
SSHTabComponent,
|
SSHTabComponent,
|
||||||
],
|
],
|
||||||
|
@@ -10,7 +10,10 @@ export class RecoveryProvider extends TabRecoveryProvider {
|
|||||||
if (recoveryToken && recoveryToken.type === 'app:ssh-tab') {
|
if (recoveryToken && recoveryToken.type === 'app:ssh-tab') {
|
||||||
return {
|
return {
|
||||||
type: SSHTabComponent,
|
type: SSHTabComponent,
|
||||||
options: { connection: recoveryToken.connection },
|
options: {
|
||||||
|
connection: recoveryToken.connection,
|
||||||
|
savedState: recoveryToken.savedState,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
|
import colors from 'ansi-colors'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Client } from 'ssh2'
|
import { Client } from 'ssh2'
|
||||||
import * as fs from 'mz/fs'
|
import * as fs from 'mz/fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import * as sshpk from 'sshpk'
|
||||||
import { ToastrService } from 'ngx-toastr'
|
import { ToastrService } from 'ngx-toastr'
|
||||||
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
||||||
import { SSHConnection, SSHSession } from '../api'
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
@@ -12,7 +14,7 @@ import { PasswordStorageService } from './passwordStorage.service'
|
|||||||
import { SSH2Stream } from 'ssh2-streams'
|
import { SSH2Stream } from 'ssh2-streams'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires
|
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -20,7 +22,7 @@ export class SSHService {
|
|||||||
private logger: Logger
|
private logger: Logger
|
||||||
|
|
||||||
private constructor (
|
private constructor (
|
||||||
log: LogService,
|
private log: LogService,
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
@@ -32,15 +34,24 @@ export class SSHService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
|
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
|
||||||
return this.zone.run(() => this.app.openNewTab(
|
const tab = this.zone.run(() => this.app.openNewTab(
|
||||||
SSHTabComponent,
|
SSHTabComponent,
|
||||||
{ connection }
|
{ connection }
|
||||||
) as SSHTabComponent)
|
) as SSHTabComponent)
|
||||||
|
if (connection.color) {
|
||||||
|
(this.app.getParentTab(tab) || tab).color = connection.color
|
||||||
|
}
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession (connection: SSHConnection): SSHSession {
|
||||||
|
const session = new SSHSession(connection)
|
||||||
|
session.logger = this.log.create(`ssh-${connection.host}-${connection.port}`)
|
||||||
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectSession (session: SSHSession, logCallback?: (s: any) => void): Promise<void> {
|
async connectSession (session: SSHSession, logCallback?: (s: any) => void): Promise<void> {
|
||||||
let privateKey: string|null = null
|
let privateKey: string|null = null
|
||||||
let privateKeyPassphrase: string|null = null
|
|
||||||
let privateKeyPath = session.connection.privateKey
|
let privateKeyPath = session.connection.privateKey
|
||||||
|
|
||||||
if (!logCallback) {
|
if (!logCallback) {
|
||||||
@@ -55,42 +66,51 @@ export class SSHService {
|
|||||||
if (!privateKeyPath) {
|
if (!privateKeyPath) {
|
||||||
const userKeyPath = path.join(process.env.HOME as string, '.ssh', 'id_rsa')
|
const userKeyPath = path.join(process.env.HOME as string, '.ssh', 'id_rsa')
|
||||||
if (await fs.exists(userKeyPath)) {
|
if (await fs.exists(userKeyPath)) {
|
||||||
log(`Using user's default private key: ${userKeyPath}`)
|
log('Using user\'s default private key')
|
||||||
privateKeyPath = userKeyPath
|
privateKeyPath = userKeyPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKeyPath) {
|
if (privateKeyPath) {
|
||||||
|
log('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||||
try {
|
try {
|
||||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('Could not read the private key file')
|
log(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
||||||
this.toastr.error('Could not read the private key file')
|
this.toastr.error('Could not read the private key file')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKey) {
|
if (privateKey) {
|
||||||
log(`Loading private key from ${privateKeyPath}`)
|
let parsedKey: any = null
|
||||||
|
try {
|
||||||
|
parsedKey = sshpk.parsePrivateKey(privateKey, 'auto')
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof sshpk.KeyEncryptedError) {
|
||||||
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
log(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 (e) { }
|
||||||
|
parsedKey = sshpk.parsePrivateKey(
|
||||||
|
privateKey,
|
||||||
|
'auto',
|
||||||
|
{ passphrase: passphrase }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let encrypted = privateKey.includes('ENCRYPTED')
|
privateKey = parsedKey!.toString('pem')
|
||||||
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
|
|
||||||
encrypted = encrypted || privateKey.includes('Encryption:') && !privateKey.includes('Encryption: none')
|
|
||||||
}
|
|
||||||
if (encrypted) {
|
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
|
||||||
log('Key requires passphrase')
|
|
||||||
modal.componentInstance.prompt = 'Private key passphrase'
|
|
||||||
modal.componentInstance.password = true
|
|
||||||
try {
|
|
||||||
const result = await modal.result
|
|
||||||
if (result) {
|
|
||||||
privateKeyPassphrase = result.value
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssh = new Client()
|
const ssh = new Client()
|
||||||
|
session.ssh = ssh
|
||||||
let connected = false
|
let connected = false
|
||||||
let savedPassword: string|null = null
|
let savedPassword: string|null = null
|
||||||
await new Promise(async (resolve, reject) => {
|
await new Promise(async (resolve, reject) => {
|
||||||
@@ -114,7 +134,7 @@ export class SSHService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||||
log(`Keyboard-interactive auth requested: ${name}`)
|
log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`)
|
||||||
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
||||||
const results: string[] = []
|
const results: string[] = []
|
||||||
for (const prompt of prompts) {
|
for (const prompt of prompts) {
|
||||||
@@ -128,11 +148,15 @@ export class SSHService {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
ssh.on('greeting', greeting => {
|
ssh.on('greeting', greeting => {
|
||||||
log('Greeting: ' + greeting)
|
if (!session.connection.skipBanner) {
|
||||||
|
log('Greeting: ' + greeting)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ssh.on('banner', banner => {
|
ssh.on('banner', banner => {
|
||||||
log('Banner: \n' + banner)
|
if (!session.connection.skipBanner) {
|
||||||
|
log(banner)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let agent: string|null = null
|
let agent: string|null = null
|
||||||
@@ -156,7 +180,6 @@ export class SSHService {
|
|||||||
username: session.connection.user,
|
username: session.connection.user,
|
||||||
password: session.connection.privateKey ? undefined : '',
|
password: session.connection.privateKey ? undefined : '',
|
||||||
privateKey: privateKey || undefined,
|
privateKey: privateKey || undefined,
|
||||||
passphrase: privateKeyPassphrase || undefined,
|
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
agent: agent || undefined,
|
agent: agent || undefined,
|
||||||
agentForward: !!agent,
|
agentForward: !!agent,
|
||||||
@@ -164,7 +187,8 @@ export class SSHService {
|
|||||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||||
readyTimeout: session.connection.readyTimeout,
|
readyTimeout: session.connection.readyTimeout,
|
||||||
hostVerifier: digest => {
|
hostVerifier: digest => {
|
||||||
log('SHA256 fingerprint: ' + digest)
|
log(colors.bgWhite(' ') + ' Host key fingerprint:')
|
||||||
|
log(colors.bgWhite(' ') + ' ' + colors.black.bgWhite(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
hostHash: 'sha256' as any,
|
hostHash: 'sha256' as any,
|
||||||
@@ -210,31 +234,6 @@ export class SSHService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
const shell: any = await new Promise<any>((resolve, reject) => {
|
|
||||||
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(shell)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
session.shell = shell
|
|
||||||
|
|
||||||
shell.on('greeting', greeting => {
|
|
||||||
log(`Shell Greeting: ${greeting}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
shell.on('banner', banner => {
|
|
||||||
log(`Shell Banner: ${banner}`)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.toastr.error(error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,54 +20,116 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/ssh2@^0.5.35":
|
"@types/ssh2@^0.5.35":
|
||||||
version "0.5.39"
|
version "0.5.40"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.39.tgz#e39ab7e38fc01337f66237ed6c42237ef3e58e3b"
|
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.40.tgz#b65465897f40c67e66e630b1f059f13a1ea7f1fa"
|
||||||
integrity sha512-MtX4tr6Jtdn/JPVsQytjB/NBeOd7JfCyf/l79KOdkUYL+r4GXUXcySX1n8W4O61fnQdljTBXEPJJ2dhnGhi2xg==
|
integrity sha512-Yrs2vFIirdscR+xY4leiUosHTClRbVBiFLlm5gA0JMZMjbCulHKO3qcgMqATElrquiZal5sSHoXMk4t8DzDawA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@types/ssh2-streams" "*"
|
"@types/ssh2-streams" "*"
|
||||||
|
|
||||||
asn1@~0.2.0:
|
ansi-colors@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||||
|
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
|
||||||
|
|
||||||
|
asn1@~0.2.0, asn1@~0.2.3:
|
||||||
version "0.2.4"
|
version "0.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||||
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
|
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer "~2.1.0"
|
safer-buffer "~2.1.0"
|
||||||
|
|
||||||
bcrypt-pbkdf@^1.0.2:
|
assert-plus@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||||
|
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||||
|
|
||||||
|
bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||||
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
|
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
|
||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
tweetnacl "^0.14.3"
|
||||||
|
|
||||||
safer-buffer@~2.1.0:
|
cli-spinner@^0.2.10:
|
||||||
|
version "0.2.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"
|
||||||
|
integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==
|
||||||
|
|
||||||
|
dashdash@^1.12.0:
|
||||||
|
version "1.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
|
||||||
|
dependencies:
|
||||||
|
assert-plus "^1.0.0"
|
||||||
|
|
||||||
|
ecc-jsbn@~0.1.1:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||||
|
integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
|
||||||
|
dependencies:
|
||||||
|
jsbn "~0.1.0"
|
||||||
|
safer-buffer "^2.1.0"
|
||||||
|
|
||||||
|
getpass@^0.1.1:
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||||
|
integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
|
||||||
|
dependencies:
|
||||||
|
assert-plus "^1.0.0"
|
||||||
|
|
||||||
|
jsbn@~0.1.0:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||||
|
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||||
|
|
||||||
|
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
ssh2-streams@^0.4.2, ssh2-streams@~0.4.7:
|
ssh2-streams@^0.4.2, ssh2-streams@~0.4.8:
|
||||||
version "0.4.7"
|
version "0.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.7.tgz#093b89069de9cf5f06feff0601a5301471b01611"
|
resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.8.tgz#2ff92df2e0063fef86cf934eaea197967deda715"
|
||||||
integrity sha512-JhF8BNfeguOqVHOLhXjzLlRKlUP8roAEhiT/y+NcBQCqpRUupLNrRf2M+549OPNVGx21KgKktug4P3MY/IvTig==
|
integrity sha512-auxXfgYySz2vYw7TMU7PK7vFI7EPvhvTH8/tZPgGaWocK4p/vwCMiV3icz9AEkb0R40kOKZtFtqYIxDJyJiytw==
|
||||||
dependencies:
|
dependencies:
|
||||||
asn1 "~0.2.0"
|
asn1 "~0.2.0"
|
||||||
bcrypt-pbkdf "^1.0.2"
|
bcrypt-pbkdf "^1.0.2"
|
||||||
streamsearch "~0.1.2"
|
streamsearch "~0.1.2"
|
||||||
|
|
||||||
ssh2@^0.8.2:
|
ssh2@^0.8.2:
|
||||||
version "0.8.6"
|
version "0.8.7"
|
||||||
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.6.tgz#dcc62e1d3b9e58a21f711f5186f043e4e792e6da"
|
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.7.tgz#2dc15206f493010b98027201cf399b90bab79c89"
|
||||||
integrity sha512-T0cPmEtmtC8WxSupicFDjx3vVUdNXO8xu2a/D5bjt8ixOUCe387AgvxU3mJgEHpu7+Sq1ZYx4d3P2pl/yxMH+w==
|
integrity sha512-/u1BO12kb0lDVxJXejWB9pxyF3/ncgRqI9vPCZuPzo05pdNDzqUeQRavScwSPsfMGK+5H/VRqp1IierIx0Bcxw==
|
||||||
dependencies:
|
dependencies:
|
||||||
ssh2-streams "~0.4.7"
|
ssh2-streams "~0.4.8"
|
||||||
|
|
||||||
|
sshpk@^1.16.1:
|
||||||
|
version "1.16.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||||
|
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
|
||||||
|
dependencies:
|
||||||
|
asn1 "~0.2.3"
|
||||||
|
assert-plus "^1.0.0"
|
||||||
|
bcrypt-pbkdf "^1.0.0"
|
||||||
|
dashdash "^1.12.0"
|
||||||
|
ecc-jsbn "~0.1.1"
|
||||||
|
getpass "^0.1.1"
|
||||||
|
jsbn "~0.1.0"
|
||||||
|
safer-buffer "^2.0.2"
|
||||||
|
tweetnacl "~0.14.0"
|
||||||
|
|
||||||
streamsearch@~0.1.2:
|
streamsearch@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
||||||
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
|
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
|
||||||
|
|
||||||
tweetnacl@^0.14.3:
|
terminus-terminal@^1.0.98-nightly.0:
|
||||||
|
version "1.0.98-nightly.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/terminus-terminal/-/terminus-terminal-1.0.98-nightly.0.tgz#10df71b0a81adf76a076fb21a91c859dd2f8bef7"
|
||||||
|
integrity sha512-JLxkeoQkORcfe6cRW6BJF5ZPSbvKA8IWUAb7fzBONVmNfRKj2Mq/uYPy76UXsdmb9F1n+rYIg+DShNp57asMKA==
|
||||||
|
|
||||||
|
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "terminus-terminal",
|
"name": "terminus-terminal",
|
||||||
"version": "1.0.93-nightly.0",
|
"version": "1.0.99-nightly.0",
|
||||||
"description": "Terminus' terminal emulation core",
|
"description": "Terminus' terminal emulation core",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminus-builtin-plugin"
|
"terminus-builtin-plugin"
|
||||||
@@ -19,19 +19,23 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/deep-equal": "^1.0.0",
|
"@types/deep-equal": "^1.0.0",
|
||||||
"@types/slug": "^0.9.1",
|
"@types/slug": "^0.9.1",
|
||||||
|
"ansi-colors": "^4.1.1",
|
||||||
"dataurl": "0.1.0",
|
"dataurl": "0.1.0",
|
||||||
"deep-equal": "1.1.0",
|
"deep-equal": "1.1.0",
|
||||||
"hterm-umdjs": "1.4.1",
|
"hterm-umdjs": "1.4.1",
|
||||||
"mz": "^2.6.0",
|
"mz": "^2.6.0",
|
||||||
"ps-node": "^0.1.6",
|
"ps-node": "^0.1.6",
|
||||||
"runes": "^0.4.2",
|
"runes": "^0.4.2",
|
||||||
"slug": "^1.1.0",
|
"slug": "^2.0.0",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "3.15.0-beta98",
|
"xterm": "^4.4.0-beta.15",
|
||||||
"xterm-addon-fit": "^0.4.0-beta1",
|
"xterm-addon-fit": "^0.4.0-beta2",
|
||||||
"xterm-addon-ligatures": "^0.2.0",
|
"xterm-addon-ligatures": "^0.2.1",
|
||||||
"xterm-addon-search": "^0.4.0-beta3",
|
"xterm-addon-search": "^0.5.0",
|
||||||
"xterm-addon-webgl": "^0.4.0-beta1"
|
"xterm-addon-serialize": "^0.1.1",
|
||||||
|
"xterm-addon-unicode11": "^0.1.1",
|
||||||
|
"xterm-addon-webgl": "^0.6.0-beta.2",
|
||||||
|
"zmodem.js": "^0.1.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/animations": "^7",
|
"@angular/animations": "^7",
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { Observable, Subject, Subscription } from 'rxjs'
|
import { Observable, Subject, Subscription } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { ToastrService } from 'ngx-toastr'
|
import { ToastrService } from 'ngx-toastr'
|
||||||
|
import colors from 'ansi-colors'
|
||||||
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input, ElementRef } from '@angular/core'
|
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input, ElementRef } from '@angular/core'
|
||||||
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
|
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
|
||||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
|
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger, TabContextMenuItemProvider } from 'terminus-core'
|
||||||
|
|
||||||
import { BaseSession, SessionsService } from '../services/sessions.service'
|
import { BaseSession, SessionsService } from '../services/sessions.service'
|
||||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||||
@@ -11,7 +12,6 @@ import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
|||||||
import { Frontend } from '../frontends/frontend'
|
import { Frontend } from '../frontends/frontend'
|
||||||
import { ResizeEvent } from './interfaces'
|
import { ResizeEvent } from './interfaces'
|
||||||
import { TerminalDecorator } from './decorator'
|
import { TerminalDecorator } from './decorator'
|
||||||
import { TerminalContextMenuItemProvider } from './contextMenuProvider'
|
|
||||||
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -22,8 +22,8 @@ export interface ToastrServiceProxy {
|
|||||||
* A class to base your custom terminal tabs on
|
* A class to base your custom terminal tabs on
|
||||||
*/
|
*/
|
||||||
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||||
static template = require('../components/baseTerminalTab.component.pug')
|
static template = require<string>('../components/baseTerminalTab.component.pug')
|
||||||
static styles = [require('../components/terminalTab.component.scss')]
|
static styles = [require<string>('../components/terminalTab.component.scss')]
|
||||||
static animations: AnimationTriggerMetadata[] = [trigger('slideInOut', [
|
static animations: AnimationTriggerMetadata[] = [trigger('slideInOut', [
|
||||||
transition(':enter', [
|
transition(':enter', [
|
||||||
style({ transform: 'translateY(-25%)' }),
|
style({ transform: 'translateY(-25%)' }),
|
||||||
@@ -35,6 +35,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
])]
|
])]
|
||||||
|
|
||||||
session: BaseSession
|
session: BaseSession
|
||||||
|
savedState: any
|
||||||
|
|
||||||
@Input() zoom = 0
|
@Input() zoom = 0
|
||||||
|
|
||||||
@Input() showSearchPanel = false
|
@Input() showSearchPanel = false
|
||||||
@@ -56,6 +58,11 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
frontendReady = new Subject<void>()
|
frontendReady = new Subject<void>()
|
||||||
size: ResizeEvent
|
size: ResizeEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables normall passthrough from session output to terminal input
|
||||||
|
*/
|
||||||
|
enablePassthrough = true
|
||||||
|
|
||||||
protected logger: Logger
|
protected logger: Logger
|
||||||
protected output = new Subject<string>()
|
protected output = new Subject<string>()
|
||||||
private sessionCloseSubscription: Subscription
|
private sessionCloseSubscription: Subscription
|
||||||
@@ -63,7 +70,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
private bellPlayer: HTMLAudioElement
|
private bellPlayer: HTMLAudioElement
|
||||||
private termContainerSubscriptions: Subscription[] = []
|
private termContainerSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
get input$ (): Observable<string> { return this.frontend.input$ }
|
get input$ (): Observable<Buffer> { return this.frontend.input$ }
|
||||||
get output$ (): Observable<string> { return this.output }
|
get output$ (): Observable<string> { return this.output }
|
||||||
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
||||||
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
||||||
@@ -83,7 +90,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
@Inject(ToastrService) protected toastr: ToastrServiceProxy,
|
@Inject(ToastrService) protected toastr: ToastrServiceProxy,
|
||||||
protected log: LogService,
|
protected log: LogService,
|
||||||
@Optional() @Inject(TerminalDecorator) protected decorators: TerminalDecorator[],
|
@Optional() @Inject(TerminalDecorator) protected decorators: TerminalDecorator[],
|
||||||
@Optional() @Inject(TerminalContextMenuItemProvider) protected contextMenuProviders: TerminalContextMenuItemProvider[],
|
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.logger = log.create('baseTerminalTab')
|
this.logger = log.create('baseTerminalTab')
|
||||||
@@ -167,6 +174,14 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.size = { columns, rows }
|
this.size = { columns, rows }
|
||||||
this.frontendReady.next()
|
this.frontendReady.next()
|
||||||
|
|
||||||
|
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||||
|
try {
|
||||||
|
decorator.attach(this)
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn('Decorator attach() throws', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.session.resize(columns, rows)
|
this.session.resize(columns, rows)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -174,6 +189,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.session.releaseInitialDataBuffer()
|
this.session.releaseInitialDataBuffer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.savedState) {
|
||||||
|
this.frontend.restoreState(this.savedState)
|
||||||
|
this.frontend.write('\r\n\r\n')
|
||||||
|
this.frontend.write(colors.bgWhite.black(' * ') + colors.bgBlackBright.white(' History restored '))
|
||||||
|
this.frontend.write('\r\n\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
if (this.hasFocus) {
|
if (this.hasFocus) {
|
||||||
this.frontend.attach(this.content.nativeElement)
|
this.frontend.attach(this.content.nativeElement)
|
||||||
@@ -190,14 +212,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
|
|
||||||
this.configure()
|
this.configure()
|
||||||
|
|
||||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
|
||||||
try {
|
|
||||||
decorator.attach(this)
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn('Decorator attach() throws', e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.output.subscribe(() => {
|
this.output.subscribe(() => {
|
||||||
this.displayActivity()
|
this.displayActivity()
|
||||||
@@ -229,7 +243,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
/**
|
/**
|
||||||
* Feeds input into the active session
|
* Feeds input into the active session
|
||||||
*/
|
*/
|
||||||
sendInput (data: string) {
|
sendInput (data: string|Buffer) {
|
||||||
|
if (!(data instanceof Buffer)) {
|
||||||
|
data = Buffer.from(data, 'utf-8')
|
||||||
|
}
|
||||||
this.session.write(data)
|
this.session.write(data)
|
||||||
if (this.config.store.terminal.scrollOnInput) {
|
if (this.config.store.terminal.scrollOnInput) {
|
||||||
this.frontend.scrollToBottom()
|
this.frontend.scrollToBottom()
|
||||||
@@ -245,7 +262,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||||
if (percentage > 0 && percentage <= 100) {
|
if (percentage > 0 && percentage <= 100) {
|
||||||
this.setProgress(percentage)
|
this.setProgress(percentage)
|
||||||
this.logger.debug('Detected progress:', percentage)
|
// this.logger.debug('Detected progress:', percentage)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setProgress(null)
|
this.setProgress(null)
|
||||||
@@ -350,7 +367,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.frontend.mouseEvent$.subscribe(async event => {
|
this.frontend.mouseEvent$.subscribe(async event => {
|
||||||
if (event.type === 'mousedown') {
|
if (event.type === 'mousedown') {
|
||||||
if (event.which === 2) {
|
if (event.which === 2) {
|
||||||
this.paste()
|
if (this.config.store.terminal.pasteOnMiddleClick) {
|
||||||
|
this.paste()
|
||||||
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return
|
return
|
||||||
@@ -405,10 +424,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
protected attachSessionHandlers () {
|
protected attachSessionHandlers () {
|
||||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||||
this.session.output$.subscribe(data => {
|
this.session.output$.subscribe(data => {
|
||||||
this.zone.run(() => {
|
if (this.enablePassthrough) {
|
||||||
this.output.next(data)
|
this.zone.run(() => {
|
||||||
this.write(data)
|
this.output.next(data)
|
||||||
})
|
this.write(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||||
|
@@ -2,6 +2,7 @@ import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend to add more terminal context menu items
|
* Extend to add more terminal context menu items
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
export abstract class TerminalContextMenuItemProvider {
|
export abstract class TerminalContextMenuItemProvider {
|
||||||
weight: number
|
weight: number
|
||||||
|
@@ -1,16 +1,35 @@
|
|||||||
|
import { Subscription } from 'rxjs'
|
||||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend to automatically run actions on new terminals
|
* Extend to automatically run actions on new terminals
|
||||||
*/
|
*/
|
||||||
export abstract class TerminalDecorator {
|
export abstract class TerminalDecorator {
|
||||||
|
private smartSubscriptions = new Map<BaseTerminalTabComponent, Subscription[]>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a new terminal tab starts
|
* Called when a new terminal tab starts
|
||||||
*/
|
*/
|
||||||
attach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line
|
attach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before a terminal tab is destroyed
|
* Called before a terminal tab is destroyed.
|
||||||
|
* Make sure to call super()
|
||||||
*/
|
*/
|
||||||
detach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line
|
detach (terminal: BaseTerminalTabComponent): void {
|
||||||
|
for (const s of this.smartSubscriptions.get(terminal) || []) {
|
||||||
|
s.unsubscribe()
|
||||||
|
}
|
||||||
|
this.smartSubscriptions.delete(terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically cancel @subscription once detached from @terminal
|
||||||
|
*/
|
||||||
|
protected subscribeUntilDetached (terminal: BaseTerminalTabComponent, subscription: Subscription) {
|
||||||
|
if (!this.smartSubscriptions.has(terminal)) {
|
||||||
|
this.smartSubscriptions.set(terminal, [])
|
||||||
|
}
|
||||||
|
this.smartSubscriptions.get(terminal)?.push(subscription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,8 +16,9 @@ export interface SessionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
name: string,
|
name: string
|
||||||
sessionOptions: SessionOptions,
|
color?: string
|
||||||
|
sessionOptions: SessionOptions
|
||||||
isBuiltin?: boolean
|
isBuiltin?: boolean
|
||||||
icon?: string
|
icon?: string
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ module.exports = function patchPTYModule (mod) {
|
|||||||
mod.spawn = (file, args, opt) => {
|
mod.spawn = (file, args, opt) => {
|
||||||
let terminal = oldSpawn(file, args, opt)
|
let terminal = oldSpawn(file, args, opt)
|
||||||
let timeout = null
|
let timeout = null
|
||||||
let buffer = ''
|
let buffer = Buffer.from('')
|
||||||
let lastFlush = 0
|
let lastFlush = 0
|
||||||
let nextTimeout = 0
|
let nextTimeout = 0
|
||||||
|
|
||||||
@@ -19,11 +19,11 @@ module.exports = function patchPTYModule (mod) {
|
|||||||
const maxWindow = 100
|
const maxWindow = 100
|
||||||
|
|
||||||
function flush () {
|
function flush () {
|
||||||
if (buffer) {
|
if (buffer.length) {
|
||||||
terminal.emit('data-buffered', buffer)
|
terminal.emit('data-buffered', buffer)
|
||||||
}
|
}
|
||||||
lastFlush = Date.now()
|
lastFlush = Date.now()
|
||||||
buffer = ''
|
buffer = Buffer.from('')
|
||||||
}
|
}
|
||||||
|
|
||||||
function reschedule () {
|
function reschedule () {
|
||||||
@@ -38,12 +38,15 @@ module.exports = function patchPTYModule (mod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal.on('data', data => {
|
terminal.on('data', data => {
|
||||||
buffer += data
|
if (typeof data === 'string') {
|
||||||
|
data = Buffer.from(data)
|
||||||
|
}
|
||||||
|
buffer = Buffer.concat([buffer, data])
|
||||||
if (Date.now() - lastFlush > maxWindow) {
|
if (Date.now() - lastFlush > maxWindow) {
|
||||||
// Taking too much time buffering, flush to keep things interactive
|
// Taking too much time buffering, flush to keep things interactive
|
||||||
flush()
|
flush()
|
||||||
} else {
|
} else {
|
||||||
if (Date.now() > nextTimeout - (maxWindow / 10)) {
|
if (Date.now() > nextTimeout - maxWindow / 10) {
|
||||||
// Extend the window if it's expiring
|
// Extend the window if it's expiring
|
||||||
reschedule()
|
reschedule()
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,11 @@
|
|||||||
h3.mb-3 Appearance
|
h3.mb-3 Appearance
|
||||||
.d-flex
|
.d-flex
|
||||||
.mr-5
|
.mr-5
|
||||||
.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
|
.form-line
|
||||||
.header
|
.header
|
||||||
.title Font
|
.title Font
|
||||||
|
|
||||||
.d-flex.w-75
|
.input-group.w-75
|
||||||
input.form-control.w-75(
|
input.form-control.w-75(
|
||||||
type='text',
|
type='text',
|
||||||
[ngbTypeahead]='fontAutocomplete',
|
[ngbTypeahead]='fontAutocomplete',
|
||||||
@@ -52,9 +39,10 @@ h3.mb-3 Appearance
|
|||||||
)
|
)
|
||||||
option(*ngFor='let scheme of config.store.terminal.customColorSchemes', [ngValue]='scheme') Custom: {{scheme.name}}
|
option(*ngFor='let scheme of config.store.terminal.customColorSchemes', [ngValue]='scheme') Custom: {{scheme.name}}
|
||||||
option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}}
|
option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}}
|
||||||
.input-group-btn
|
.input-group-append
|
||||||
button.btn.btn-secondary((click)='editScheme(config.store.terminal.colorScheme)') Edit
|
button.btn.btn-secondary((click)='editScheme(config.store.terminal.colorScheme)')
|
||||||
.input-group-btn
|
i.fas.fa-pen
|
||||||
|
.input-group-append
|
||||||
button.btn.btn-outline-danger(
|
button.btn.btn-outline-danger(
|
||||||
(click)='deleteScheme(config.store.terminal.colorScheme)',
|
(click)='deleteScheme(config.store.terminal.colorScheme)',
|
||||||
*ngIf='isCustomScheme(config.store.terminal.colorScheme)'
|
*ngIf='isCustomScheme(config.store.terminal.colorScheme)'
|
||||||
@@ -65,10 +53,12 @@ h3.mb-3 Appearance
|
|||||||
label Editing
|
label Editing
|
||||||
.input-group
|
.input-group
|
||||||
input.form-control(type='text', [(ngModel)]='editingColorScheme.name')
|
input.form-control(type='text', [(ngModel)]='editingColorScheme.name')
|
||||||
.input-group-btn
|
.input-group-append
|
||||||
button.btn.btn-secondary((click)='saveScheme()') Save
|
button.btn.btn-secondary((click)='saveScheme()')
|
||||||
.input-group-btn
|
i.fas.fa-check
|
||||||
button.btn.btn-secondary((click)='cancelEditing()') Cancel
|
.input-group-append
|
||||||
|
button.btn.btn-secondary((click)='cancelEditing()')
|
||||||
|
i.fas.fa-times
|
||||||
|
|
||||||
.form-group(*ngIf='editingColorScheme')
|
.form-group(*ngIf='editingColorScheme')
|
||||||
color-picker(
|
color-picker(
|
||||||
@@ -180,6 +170,19 @@ h3.mb-3 Appearance
|
|||||||
span rm -rf /
|
span rm -rf /
|
||||||
span([style.background-color]='config.store.terminal.colorScheme.cursor')
|
span([style.background-color]='config.store.terminal.colorScheme.cursor')
|
||||||
|
|
||||||
|
.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
|
.form-line
|
||||||
.header
|
.header
|
||||||
.title Terminal background
|
.title Terminal background
|
||||||
|
@@ -5,10 +5,9 @@ import deepEqual from 'deep-equal'
|
|||||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||||
|
|
||||||
import { Component, Inject } from '@angular/core'
|
import { Component, Inject } from '@angular/core'
|
||||||
import { ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
|
import { ConfigService, HostAppService, Platform, ElectronService, getCSSFontFamily } from 'terminus-core'
|
||||||
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
|
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
|
||||||
import { TerminalColorScheme } from '../api/interfaces'
|
import { TerminalColorScheme } from '../api/interfaces'
|
||||||
import { getCSSFontFamily } from '../utils'
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -2,55 +2,64 @@
|
|||||||
.form-group
|
.form-group
|
||||||
label Name
|
label Name
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
autofocus,
|
autofocus,
|
||||||
[(ngModel)]='profile.name',
|
[(ngModel)]='profile.name',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Command
|
label Command
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='profile.sessionOptions.command',
|
[(ngModel)]='profile.sessionOptions.command',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Arguments
|
label Arguments
|
||||||
.input-group(
|
.input-group(
|
||||||
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
|
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
|
||||||
)
|
)
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='profile.sessionOptions.args[i]',
|
[(ngModel)]='profile.sessionOptions.args[i]',
|
||||||
)
|
)
|
||||||
.input-group-btn
|
.input-group-append
|
||||||
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
|
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
|
||||||
i.fas.fa-trash
|
i.fas.fa-trash
|
||||||
|
|
||||||
.mt-2
|
.mt-2
|
||||||
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
|
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
|
||||||
i.fas.fa-plus.mr-2
|
i.fas.fa-plus.mr-2
|
||||||
| Add
|
| Add
|
||||||
|
|
||||||
.form-line(*ngIf='uac.isAvailable')
|
.form-line(*ngIf='uac.isAvailable')
|
||||||
.header
|
.header
|
||||||
.title Run as administrator
|
.title Run as administrator
|
||||||
toggle(
|
toggle(
|
||||||
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
|
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Working directory
|
label Working directory
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='profile.sessionOptions.cwd',
|
[(ngModel)]='profile.sessionOptions.cwd',
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label Environment
|
label Environment
|
||||||
environment-editor(
|
environment-editor(
|
||||||
type='text',
|
type='text',
|
||||||
[(model)]='profile.sessionOptions.env',
|
[(model)]='profile.sessionOptions.env',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label Tab color
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
autofocus,
|
||||||
|
[(ngModel)]='profile.color',
|
||||||
|
placeholder='#000000'
|
||||||
)
|
)
|
||||||
|
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
|
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
|
||||||
.input-group.w-50
|
.input-group
|
||||||
input.form-control([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
|
input.form-control.w-25([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
|
||||||
.input-group-append
|
.input-group-append
|
||||||
.input-group-text =
|
.input-group-text =
|
||||||
input.form-control.w-50.mr-1([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
|
input.form-control.w-50([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
|
||||||
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
|
.input-group-append
|
||||||
i.fas.fa-trash
|
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
|
||||||
|
i.fas.fa-trash
|
||||||
|
|
||||||
button.btn.btn-secondary((click)='addEnvironmentVar()')
|
button.btn.btn-secondary((click)='addEnvironmentVar()')
|
||||||
i.fas.fa-plus.mr-2
|
i.fas.fa-plus.mr-2
|
||||||
span Add
|
span Add
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
input.search-input.form-control(
|
input.search-input.form-control(
|
||||||
type='search',
|
type='search',
|
||||||
[(ngModel)]='query',
|
[(ngModel)]='query',
|
||||||
(ngModelChange)='notFound = false',
|
(ngModelChange)='onQueryChange()',
|
||||||
[class.text-danger]='notFound',
|
[class.text-danger]='notFound',
|
||||||
(click)='$event.stopPropagation()',
|
(click)='$event.stopPropagation()',
|
||||||
(keyup.enter)='findNext()',
|
(keyup.enter)='findNext()',
|
||||||
@@ -10,31 +10,27 @@
|
|||||||
placeholder='Search...'
|
placeholder='Search...'
|
||||||
)
|
)
|
||||||
.input-group-append
|
.input-group-append
|
||||||
.btn-group
|
.input-group-text
|
||||||
button.btn.btn-outline-primary(
|
a(
|
||||||
(click)='options.caseSensitive = !options.caseSensitive',
|
(click)='options.caseSensitive = !options.caseSensitive',
|
||||||
[class.active]='options.caseSensitive',
|
[class.text-info]='options.caseSensitive',
|
||||||
ngbTooltip='Case sensitivity',
|
ngbTooltip='Case sensitivity',
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
)
|
)
|
||||||
i.fa.fa-fw.fa-font
|
i.fa.fa-fw.fa-font
|
||||||
button.btn.btn-outline-primary(
|
a(
|
||||||
(click)='options.regex = !options.regex',
|
(click)='options.regex = !options.regex',
|
||||||
[class.active]='options.regex',
|
[class.text-info]='options.regex',
|
||||||
ngbTooltip='Regular expression',
|
ngbTooltip='Regular expression',
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
)
|
)
|
||||||
i.fa.fa-fw.fa-asterisk
|
i.fa.fa-fw.fa-asterisk
|
||||||
button.btn.btn-outline-primary(
|
a(
|
||||||
(click)='options.wholeWord = !options.wholeWord',
|
(click)='options.wholeWord = !options.wholeWord',
|
||||||
[class.active]='options.wholeWord',
|
[class.text-info]='options.wholeWord',
|
||||||
ngbTooltip='Whole word',
|
ngbTooltip='Whole word',
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
)
|
)
|
||||||
i.fa.fa-fw.fa-text-width
|
i.fa.fa-fw.fa-text-width
|
||||||
button.btn.btn-outline(
|
|
||||||
(click)='close.emit()',
|
button.close.text-light.pl-3.pr-2((click)='close.emit()') ×
|
||||||
ngbTooltip='Close',
|
|
||||||
placement='bottom'
|
|
||||||
)
|
|
||||||
i.fa.fa-fw.fa-times
|
|
||||||
|
@@ -1,9 +1,20 @@
|
|||||||
:host {
|
:host {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
align-self: center;
|
right: 50px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 0 0 3px 3px;
|
border-radius: 0 0 3px 3px;
|
||||||
background: rgba(0, 0, 0, .75);
|
background: rgba(0, 0, 0, .75);
|
||||||
|
border: 1px solid rgba(0, 0, 0, .5);
|
||||||
|
border-top: 0;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding-left: 10px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,9 @@ export class SearchPanelComponent {
|
|||||||
@Input() query: string
|
@Input() query: string
|
||||||
@Input() frontend: Frontend
|
@Input() frontend: Frontend
|
||||||
notFound = false
|
notFound = false
|
||||||
options: SearchOptions = {}
|
options: SearchOptions = {
|
||||||
|
incremental: true,
|
||||||
|
}
|
||||||
|
|
||||||
@Output() close = new EventEmitter()
|
@Output() close = new EventEmitter()
|
||||||
|
|
||||||
@@ -19,8 +21,13 @@ export class SearchPanelComponent {
|
|||||||
private toastr: ToastrService,
|
private toastr: ToastrService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
findNext () {
|
onQueryChange () {
|
||||||
if (!this.frontend.findNext(this.query, this.options)) {
|
this.notFound = false
|
||||||
|
this.findNext(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
findNext (incremental = false) {
|
||||||
|
if (!this.frontend.findNext(this.query, { ...this.options, incremental: incremental || undefined })) {
|
||||||
this.notFound = true
|
this.notFound = true
|
||||||
this.toastr.error('Not found')
|
this.toastr.error('Not found')
|
||||||
}
|
}
|
||||||
|
@@ -14,20 +14,20 @@ h3.mb-3 Shell
|
|||||||
[ngValue]='slug(profile.name).toLowerCase()'
|
[ngValue]='slug(profile.name).toLowerCase()'
|
||||||
) {{profile.name}}
|
) {{profile.name}}
|
||||||
|
|
||||||
|
|
||||||
.form-line(*ngIf='isConPTYAvailable')
|
.form-line(*ngIf='isConPTYAvailable')
|
||||||
.header
|
.header
|
||||||
.title Use ConPTY
|
.title Use ConPTY
|
||||||
.description Enables the experimental Windows ConPTY API
|
.description Enables the experimental Windows ConPTY API
|
||||||
|
|
||||||
toggle(
|
toggle(
|
||||||
[(ngModel)]='config.store.terminal.useConPTY',
|
[(ngModel)]='config.store.terminal.useConPTY',
|
||||||
(ngModelChange)='config.save()'
|
(ngModelChange)='config.save()'
|
||||||
)
|
)
|
||||||
|
|
||||||
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.useConPTY && isConPTYAvailable && !isConPTYStable')
|
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.useConPTY && isConPTYAvailable && !isConPTYStable')
|
||||||
.mr-auto Windows 10 build 18309 or above is recommended for ConPTY
|
.mr-auto Windows 10 build 18309 or above is recommended for ConPTY
|
||||||
|
|
||||||
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.profile.startsWith("WSL") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
|
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.profile.startsWith("WSL") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
|
||||||
.mr-auto WSL terminal only supports TrueColor with ConPTY and the hterm frontend
|
.mr-auto WSL terminal only supports TrueColor with ConPTY and the hterm frontend
|
||||||
|
|
||||||
@@ -50,15 +50,17 @@ h3.mb-3 Shell
|
|||||||
placeholder='Home directory',
|
placeholder='Home directory',
|
||||||
[(ngModel)]='config.store.terminal.workingDirectory',
|
[(ngModel)]='config.store.terminal.workingDirectory',
|
||||||
(ngModelChange)='config.save()',
|
(ngModelChange)='config.save()',
|
||||||
)
|
)
|
||||||
.input-group-btn
|
.input-group-append
|
||||||
button.btn.btn-secondary((click)='pickWorkingDirectory()')
|
button.btn.btn-secondary((click)='pickWorkingDirectory()')
|
||||||
i.fas.fa-folder-open
|
i.fas.fa-folder-open
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
.title Always Use Working Directory
|
.title Always Use Working Directory
|
||||||
.description By default, new terminals will open where the previous terminal was working. Enabling this option will always launch new terminals in the working directory specified above.
|
.description
|
||||||
|
div By default, new terminals will open where the previous terminal was working.
|
||||||
|
div Enabling this option will always launch new terminals in the working directory specified above.
|
||||||
|
|
||||||
toggle(
|
toggle(
|
||||||
[(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory',
|
[(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory',
|
||||||
@@ -69,7 +71,7 @@ h3.mb-3 Shell
|
|||||||
.header
|
.header
|
||||||
.title Environment
|
.title Environment
|
||||||
.description Inject additional environment variables
|
.description Inject additional environment variables
|
||||||
|
|
||||||
environment-editor([(model)]='this.config.store.terminal.environment')
|
environment-editor([(model)]='this.config.store.terminal.environment')
|
||||||
|
|
||||||
h3.mt-3 Saved Profiles
|
h3.mt-3 Saved Profiles
|
||||||
@@ -78,16 +80,16 @@ h3.mt-3 Saved Profiles
|
|||||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
*ngFor='let profile of config.store.terminal.profiles',
|
*ngFor='let profile of config.store.terminal.profiles',
|
||||||
(click)='editProfile(profile)',
|
(click)='editProfile(profile)',
|
||||||
)
|
)
|
||||||
.mr-auto
|
.mr-auto
|
||||||
div {{profile.name}}
|
div {{profile.name}}
|
||||||
.text-muted {{profile.sessionOptions.command}}
|
.text-muted {{profile.sessionOptions.command}}
|
||||||
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteProfile(profile)')
|
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteProfile(profile)')
|
||||||
i.fas.fa-trash
|
i.fas.fa-trash
|
||||||
|
|
||||||
div(ngbDropdown, placement='top-left')
|
div(ngbDropdown, placement='top-left')
|
||||||
button.btn.btn-primary(ngbDropdownToggle)
|
button.btn.btn-primary(ngbDropdownToggle)
|
||||||
i.fas.fa-fw.fa-plus
|
i.fas.fa-fw.fa-plus
|
||||||
| New profile
|
| New profile
|
||||||
div(ngbDropdownMenu)
|
div(ngbDropdownMenu)
|
||||||
button.dropdown-item(*ngFor='let shell of shells', (click)='newProfile(shell)') {{shell.name}}
|
button.dropdown-item(*ngFor='let shell of shells', (click)='newProfile(shell)') {{shell.name}}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user