Compare commits

...

111 Commits

Author SHA1 Message Date
Eugene Pankov
b1752bd0b4 updated cwd settings UI 2021-02-07 13:44:17 +01:00
Eugene Pankov
1e697a952a upgraded xterm.js 2021-02-07 13:44:08 +01:00
Eugene Pankov
6bad2a2167 cache dscl output 2021-02-07 13:43:58 +01:00
Eugene Pankov
61a46e3b4a lint 2021-02-07 13:15:24 +01:00
Eugene Pankov
cba90cec0a better error messages for X11 forwarding issues 2021-02-07 12:49:34 +01:00
Eugene Pankov
7583d92747 set terminus to use built-in graphics by default - fixes #657 2021-02-07 12:49:22 +01:00
Eugene Pankov
e2b99d71ad lint 2021-02-06 17:47:17 +01:00
Eugene Pankov
aac38fa190 CWD detection on windows where possible 2021-02-06 16:45:38 +01:00
Eugene Pankov
7098622c8f bumped electron 2021-02-06 11:20:34 +01:00
Eugene Pankov
8695003c74 lint 2021-02-01 11:40:49 +01:00
Eugene Pankov
43a27a7b7c serial newline modes 2021-01-31 20:07:26 +01:00
Eugene Pankov
dd2d2ce20d Update tsconfig.json 2021-01-31 19:33:33 +01:00
Eugene Pankov
73574374f0 readline input mode for serial terminals - #3099, #2661 2021-01-31 19:33:30 +01:00
Eugene Pankov
5bd1bfd565 better editor ts support 2021-01-31 18:20:58 +01:00
Eugene Pankov
0611afa8b5 better session handlers behaviour, added serial auto-reconnection logic - #3099 2021-01-31 18:20:39 +01:00
Eugene Pankov
91c9e8affd bumped angular 2021-01-28 22:01:42 +01:00
Eugene Pankov
322ffc5847 margin fix 2021-01-28 21:55:29 +01:00
Eugene Pankov
21084b5d24 bumped js-yaml 2021-01-28 21:52:11 +01:00
Eugene Pankov
e0efb4073a bumped eslint 2021-01-28 21:46:31 +01:00
Eugene Pankov
c42b62afe6 bumped deps 2021-01-28 21:11:53 +01:00
Eugene Pankov
e2b11c83d5 fixed cli args handling 2021-01-28 21:11:47 +01:00
Eugene Pankov
891fa5770a Update electron-builder.yml 2021-01-28 18:45:09 +01:00
Eugene Pankov
6b395cc2b3 fixed windows native module import 2021-01-28 18:20:45 +01:00
Eugene Pankov
448a1a094f fixed macOS native module builds 2021-01-27 00:26:48 +01:00
Eugene Pankov
788dd61a13 exclude native modules from app.asar and sign them 2021-01-26 15:29:18 +01:00
Eugene Pankov
467684d9ab Update api.ts 2021-01-25 21:35:00 +01:00
Eugene Pankov
5069070040 added a 'copy current path' hotkey and context menu item - fixes #2586 2021-01-25 17:30:44 +01:00
Eugene Pankov
ecf5297bc3 Merge branch 'master' of github.com:Eugeny/terminus 2021-01-24 19:27:42 +01:00
Eugene Pankov
78bd90ac55 fixed window re-focusing on linux 2021-01-24 19:27:36 +01:00
Eugene Pankov
712589eb93 fixed #1510 2021-01-24 19:06:51 +01:00
Eugene Pankov
f103e71285 better handling of CLI args - fixes #1436 2021-01-24 19:06:41 +01:00
Eugene Pankov
0cf883cc4a bumped electron 2021-01-24 13:58:47 +01:00
Eugene Pankov
2b0ad0d558 ssh: 15s default keepalive interval 2021-01-24 13:33:54 +01:00
Eugene Pankov
67bacb9dd3 ssh: better session close and reconnect behaviours - fixes #3351, fixes #3010, fixes #3276, fixes #3074, fixes #2825, fixes #3285 2021-01-24 13:28:59 +01:00
Eugene Pankov
d0a597634d ssh: fixed clearing jump host in connection settings 2021-01-24 13:08:07 +01:00
Eugene Pankov
322014c409 Merge branch 'master' of github.com:Eugeny/terminus 2021-01-24 12:43:02 +01:00
Eugene Pankov
c751a8725b rollback node-pty to pre-napi state 2021-01-24 12:42:59 +01:00
Eugene Pankov
5417efe558 ssh: blacklist broken kex algorithms 2021-01-24 11:26:43 +01:00
Eugene
bf356fcd19 Merge pull request #3362 from mi544/readme-fix
Removes unnecessary asterisks from README
2021-01-22 09:37:05 +01:00
Maksim Verkhoturov
10ee66b9dd removes unnecessary asterisks 2021-01-21 15:45:37 -07:00
Eugene
763da0d80c Merge pull request #3310 from Eugeny/dependabot/npm_and_yarn/app/npm-6.14.11
Bump npm from 6.14.10 to 6.14.11 in /app
2021-01-15 15:12:16 +01:00
Eugene
8d46bb2181 Merge pull request #3290 from Eugeny/dependabot/npm_and_yarn/terminus-core/core-js-3.8.2
Bump core-js from 3.7.0 to 3.8.2 in /terminus-core
2021-01-15 15:12:05 +01:00
Eugene Pankov
fe936c7726 fixed #3333 - duplicate color schemes 2021-01-14 09:48:58 +01:00
Eugene Pankov
2f3e32990a Update macos.yml 2021-01-13 13:52:23 +01:00
Eugene Pankov
22344f8d54 Update macos.yml 2021-01-13 10:50:34 +01:00
Eugene Pankov
f6d37a39f4 Update macos.yml 2021-01-12 18:20:17 +01:00
dependabot-preview[bot]
0e4c60ad4b Bump npm from 6.14.10 to 6.14.11 in /app
Bumps [npm](https://github.com/npm/cli) from 6.14.10 to 6.14.11.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/v6.14.11/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.14.10...v6.14.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-12 17:01:34 +00:00
dependabot-preview[bot]
e8c2171d8f Bump core-js from 3.7.0 to 3.8.2 in /terminus-core
Bumps [core-js](https://github.com/zloirock/core-js) from 3.7.0 to 3.8.2.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.7.0...v3.8.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-12 17:01:01 +00:00
Eugene Pankov
f7a5be2c67 downgraded electron 2021-01-12 17:59:13 +01:00
Eugene Pankov
39fa0424a6 handle auto-updater errors 2021-01-09 18:22:58 +01:00
Eugene Pankov
bcb1b6a13b fixed tab scrolling 2021-01-09 17:48:31 +01:00
Eugene Pankov
a19f35ac44 fixed macos builds 2021-01-09 17:30:23 +01:00
Eugene Pankov
ea92f1a700 Revert "vibrancy fixes"
This reverts commit a8d78ce185.
2021-01-09 17:29:37 +01:00
Eugene Pankov
b5701cf9f9 fixed linux crashes 2021-01-09 17:29:34 +01:00
Eugene Pankov
4742530cf3 fixed invisible tab bar on the bottom in full-titlebar mode - fixes #3191 2021-01-09 17:11:05 +01:00
Eugene Pankov
a8d78ce185 vibrancy fixes 2021-01-09 17:02:01 +01:00
Eugene Pankov
bba1eaccbe fixed hotkeys page crash - fixes #3311 2021-01-09 16:15:20 +01:00
Eugene Pankov
cd6d05aa69 Merge branch 'master' of github.com:Eugeny/terminus 2021-01-09 15:38:58 +01:00
Eugene Pankov
412403c72a updated glasstron 2021-01-09 15:38:55 +01:00
Eugene
93ae907dd1 Merge pull request #3304 from Eugeny/all-contributors/add-frauhottelmann 2021-01-06 10:34:38 +01:00
allcontributors[bot]
ce24b9cc52 docs: update .all-contributorsrc [skip ci] 2021-01-06 09:32:42 +00:00
allcontributors[bot]
dc68372d76 docs: update README.md [skip ci] 2021-01-06 09:32:40 +00:00
Eugene
e0fe125cf2 Merge pull request #3303 from frauhottelmann/master
add baudrate 1500000
2021-01-06 10:32:21 +01:00
frauhottelmann
e594fcd0e7 add baudrate 1500000
1500000bps is needed for single board computers with RK3399 CPUs
2021-01-06 10:19:18 +01:00
Eugene Pankov
0219da4d85 sourcemap separation 2021-01-04 21:31:33 +01:00
Eugene Pankov
5e06b2248b better sentry upload 2021-01-04 20:44:24 +01:00
Eugene Pankov
cdc3623986 sentry wip 2021-01-04 20:02:47 +01:00
Eugene Pankov
6d016002c0 sentry symbols on linux 2021-01-04 19:39:59 +01:00
Eugene Pankov
f3569f5d2d sentry symbols on win 2021-01-04 19:21:12 +01:00
Eugene Pankov
4125582ef2 sentry symbols on mac 2021-01-04 19:11:57 +01:00
Eugene Pankov
c6331c9b1c fixed AppService.selectTab crash 2021-01-03 20:53:53 +01:00
Eugene Pankov
aaab475e5f fixed ssh sessions - fixes #3282 2021-01-03 18:33:03 +01:00
Eugene Pankov
e6bf76c616 reenabled @typescript-eslint/no-shadow 2021-01-02 20:53:34 +01:00
Eugene Pankov
e36bad2553 reenabled @typescript-eslint/no-unnecessary-type-assertion 2021-01-02 20:48:37 +01:00
Eugene Pankov
154cc29333 reenabled @typescript-eslint/no-untyped-public-signature 2021-01-02 20:38:45 +01:00
Eugene Pankov
1b0402c2cf reenabled @typescript-eslint/no-base-to-string 2021-01-02 20:33:32 +01:00
Eugene Pankov
15073cbc81 reenabled @typescript-eslint/no-dynamic-delete 2021-01-02 20:31:26 +01:00
Eugene Pankov
3365b143d8 fixes 2021-01-02 20:24:26 +01:00
Eugene Pankov
4d9cc91e91 reenabled @typescript-eslint/no-unnecessary-condition 2021-01-02 20:10:00 +01:00
Eugene Pankov
946f4292ef reenabled @typescript-eslint/prefer-nullish-coalescing 2021-01-02 19:09:34 +01:00
Eugene
eb12b1ae60 Merge pull request #3245 from Eugeny/dependabot/npm_and_yarn/style-loader-2.0.0
Bump style-loader from 1.3.0 to 2.0.0
2021-01-02 18:17:17 +01:00
Eugene
4765c97d31 Merge pull request #3173 from Eugeny/dependabot/npm_and_yarn/terminus-ssh/types/ssh2-0.5.46
Bump @types/ssh2 from 0.5.44 to 0.5.46 in /terminus-ssh
2021-01-02 18:16:20 +01:00
Eugene Pankov
3fb32e1a97 allow changing installation path - fixes #3267 2021-01-02 13:15:27 +01:00
Eugene Pankov
9ec1a0d253 theme fix 2021-01-02 13:12:12 +01:00
Eugene Pankov
fef19615bb fixed macOS vibrancy 2021-01-02 13:08:00 +01:00
Eugene Pankov
4d237baf33 Update hostApp.service.ts 2021-01-02 12:19:08 +01:00
Eugene Pankov
03e654b5a0 further reduce bundle 2021-01-02 11:49:56 +01:00
Eugene Pankov
ef815eaa40 Merge branch 'master' of github.com:Eugeny/terminus 2021-01-02 11:49:49 +01:00
Eugene Pankov
4771a38747 added nightly links for all platforms 2021-01-02 11:49:47 +01:00
Eugene
ce016793d4 Merge pull request #3079 from Eugeny/dependabot/npm_and_yarn/npm-user-validate-1.0.1
Bump npm-user-validate from 1.0.0 to 1.0.1
2021-01-02 11:19:12 +01:00
Eugene
3a854f04e1 Merge pull request #3233 from Eugeny/dependabot/npm_and_yarn/tar-fs-1.16.3
Bump tar-fs from 1.15.3 to 1.16.3
2021-01-02 11:18:41 +01:00
Eugene
b5658d61d9 Merge pull request #3234 from Eugeny/dependabot/npm_and_yarn/fstream-1.0.12
Bump fstream from 1.0.11 to 1.0.12
2021-01-02 11:18:24 +01:00
Eugene
02750d8581 Merge pull request #3231 from Eugeny/dependabot/npm_and_yarn/http-proxy-agent-2.1.0
Bump http-proxy-agent from 2.0.0 to 2.1.0
2021-01-02 11:18:12 +01:00
Eugene
077a3e6bba Merge pull request #3232 from Eugeny/dependabot/npm_and_yarn/stringstream-0.0.6
Bump stringstream from 0.0.5 to 0.0.6
2021-01-02 11:17:38 +01:00
Eugene Pankov
5454be032a Update windows.yml 2021-01-02 11:03:20 +01:00
Eugene Pankov
8a0b4f82db leaner build 2021-01-02 10:41:04 +01:00
Eugene Pankov
74fd1aeea5 Update linux.yml 2021-01-02 10:33:39 +01:00
Eugene Pankov
aac230e362 Update macos.yml 2021-01-02 10:20:44 +01:00
dependabot[bot]
ae82ed4a47 Bump http-proxy-agent from 2.0.0 to 2.1.0
Bumps [http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/TooTallNate/node-http-proxy-agent/releases)
- [Changelog](https://github.com/TooTallNate/node-http-proxy-agent/blob/2.1.0/History.md)
- [Commits](https://github.com/TooTallNate/node-http-proxy-agent/compare/2.0.0...2.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-01 22:13:13 +00:00
dependabot[bot]
9d1b0e9861 Bump npm-user-validate from 1.0.0 to 1.0.1
Bumps [npm-user-validate](https://github.com/npm/npm-user-validate) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/npm/npm-user-validate/releases)
- [Commits](https://github.com/npm/npm-user-validate/compare/v1.0.0...v1.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-01 22:13:10 +00:00
Eugene Pankov
8cb4e9f27d wip 2021-01-01 22:27:20 +01:00
Eugene Pankov
c8c00a2c9b wip 2021-01-01 22:23:27 +01:00
Eugene Pankov
bacb475896 merged node-pty n-api migration - potentially fixing #3261 2021-01-01 14:30:45 +01:00
Eugene Pankov
c8faa67083 fixed plugin installation - fixes #3264 2020-12-30 19:43:37 +01:00
dependabot[bot]
b6c0e3cdfb Bump tar-fs from 1.15.3 to 1.16.3
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 1.15.3 to 1.16.3.
- [Release notes](https://github.com/mafintosh/tar-fs/releases)
- [Commits](https://github.com/mafintosh/tar-fs/compare/v1.15.3...v1.16.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-30 18:37:57 +00:00
Eugene
323581d513 Merge pull request #3235 from Eugeny/dependabot/npm_and_yarn/electron-builder-22.10.4
Bump electron-builder from 22.10.3 to 22.10.4
2020-12-30 19:37:02 +01:00
dependabot-preview[bot]
21b81f476c Bump electron-builder from 22.10.3 to 22.10.4
Bumps [electron-builder](https://github.com/electron-userland/electron-builder) from 22.10.3 to 22.10.4.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/compare/v22.10.3...v22.10.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-27 12:06:23 +00:00
dependabot-preview[bot]
2283a5dad9 Bump style-loader from 1.3.0 to 2.0.0
Bumps [style-loader](https://github.com/webpack-contrib/style-loader) from 1.3.0 to 2.0.0.
- [Release notes](https://github.com/webpack-contrib/style-loader/releases)
- [Changelog](https://github.com/webpack-contrib/style-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/style-loader/compare/v1.3.0...v2.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-25 04:26:28 +00:00
dependabot-preview[bot]
d7565e497d Bump @types/ssh2 from 0.5.44 to 0.5.46 in /terminus-ssh
Bumps [@types/ssh2](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ssh2) from 0.5.44 to 0.5.46.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ssh2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-24 17:45:09 +00:00
dependabot[bot]
7b4e99fc5f Bump fstream from 1.0.11 to 1.0.12
Bumps [fstream](https://github.com/npm/fstream) from 1.0.11 to 1.0.12.
- [Release notes](https://github.com/npm/fstream/releases)
- [Commits](https://github.com/npm/fstream/compare/v1.0.11...v1.0.12)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-24 17:44:21 +00:00
dependabot[bot]
035a6f8da8 Bump stringstream from 0.0.5 to 0.0.6
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-24 17:44:15 +00:00
110 changed files with 4438 additions and 2215 deletions

View File

@@ -334,6 +334,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "frauhottelmann",
"name": "frauhottelmann",
"avatar_url": "https://avatars2.githubusercontent.com/u/902705?v=4",
"profile": "https://github.com/frauhottelmann",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -1,6 +1,8 @@
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser'
parserOptions: parserOptions:
project: tsconfig.json project:
- tsconfig.json
- '*/tsconfig.typings.json'
extends: extends:
- 'plugin:@typescript-eslint/all' - 'plugin:@typescript-eslint/all'
plugins: plugins:
@@ -29,7 +31,6 @@ rules:
'@typescript-eslint/no-magic-numbers': off '@typescript-eslint/no-magic-numbers': off
'@typescript-eslint/member-delimiter-style': off '@typescript-eslint/member-delimiter-style': off
'@typescript-eslint/promise-function-async': off '@typescript-eslint/promise-function-async': off
'@typescript-eslint/no-unnecessary-type-assertion': off
'@typescript-eslint/require-array-sort-compare': off '@typescript-eslint/require-array-sort-compare': off
'@typescript-eslint/no-floating-promises': off '@typescript-eslint/no-floating-promises': off
'@typescript-eslint/prefer-readonly': off '@typescript-eslint/prefer-readonly': off
@@ -38,6 +39,7 @@ rules:
'@typescript-eslint/no-misused-promises': off '@typescript-eslint/no-misused-promises': off
'@typescript-eslint/typedef': off '@typescript-eslint/typedef': off
'@typescript-eslint/consistent-type-imports': off '@typescript-eslint/consistent-type-imports': off
'@typescript-eslint/sort-type-union-intersection-members': off
'@typescript-eslint/no-use-before-define': '@typescript-eslint/no-use-before-define':
- error - error
- classes: false - classes: false
@@ -82,7 +84,8 @@ rules:
argsIgnorePattern: ^_ argsIgnorePattern: ^_
no-undef: error no-undef: error
no-var: error no-var: error
object-curly-spacing: object-curly-spacing: off
'@typescript-eslint/object-curly-spacing':
- error - error
- always - always
quote-props: quote-props:
@@ -95,25 +98,22 @@ rules:
- error - error
- single - single
- allowTemplateLiterals: true - allowTemplateLiterals: true
'@typescript-eslint/no-confusing-void-expression': off
'@typescript-eslint/no-non-null-assertion': off '@typescript-eslint/no-non-null-assertion': off
'@typescript-eslint/no-unnecessary-condition': off '@typescript-eslint/no-unnecessary-condition':
'@typescript-eslint/no-untyped-public-signature': off # bugs out on constructors - error
- allowConstantLoopConditions: true
'@typescript-eslint/restrict-template-expressions': off '@typescript-eslint/restrict-template-expressions': off
'@typescript-eslint/no-dynamic-delete': off
'@typescript-eslint/prefer-nullish-coalescing': off
'@typescript-eslint/prefer-readonly-parameter-types': off '@typescript-eslint/prefer-readonly-parameter-types': off
'@typescript-eslint/no-unsafe-member-access': off '@typescript-eslint/no-unsafe-member-access': off
'@typescript-eslint/no-unsafe-call': off '@typescript-eslint/no-unsafe-call': off
'@typescript-eslint/no-unsafe-return': off '@typescript-eslint/no-unsafe-return': off
'@typescript-eslint/no-base-to-string': off # broken in typescript-eslint
'@typescript-eslint/no-unsafe-assignment': off '@typescript-eslint/no-unsafe-assignment': off
'@typescript-eslint/naming-convention': off '@typescript-eslint/naming-convention': off
'@typescript-eslint/lines-between-class-members': '@typescript-eslint/lines-between-class-members':
- error - error
- exceptAfterSingleLine: true - exceptAfterSingleLine: true
'@typescript-eslint/dot-notation': off '@typescript-eslint/dot-notation': off
'@typescript-eslint/no-confusing-void-expression': off
'@typescript-eslint/no-implicit-any-catch': off '@typescript-eslint/no-implicit-any-catch': off
'@typescript-eslint/member-ordering': off '@typescript-eslint/member-ordering': off
'@typescript-eslint/no-var-requires': off '@typescript-eslint/no-var-requires': off
'@typescript-eslint/no-shadow': off

View File

@@ -36,6 +36,16 @@ jobs:
env: env:
DEBUG: electron-builder,electron-builder:* DEBUG: electron-builder,electron-builder:*
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
USE_HARD_LINKS: false
- name: Upload symbols
run: |
sudo npm install -g @sentry/cli --unsafe-perm
./scripts/sentry-upload.js
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
- name: Package artifacts - name: Package artifacts
run: | run: |

View File

@@ -26,6 +26,10 @@ jobs:
cd .. cd ..
rm app/node_modules/.yarn-integrity rm app/node_modules/.yarn-integrity
yarn yarn
./node_modules/.bin/patch-package
cd app
../node_modules/.bin/patch-package
cd ..
- name: Build native deps - name: Build native deps
run: scripts/build-native.js run: scripts/build-native.js
@@ -52,6 +56,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPSTORE_USERNAME: ${{ secrets.APPSTORE_USERNAME }} APPSTORE_USERNAME: ${{ secrets.APPSTORE_USERNAME }}
APPSTORE_PASSWORD: ${{ secrets.APPSTORE_PASSWORD }} APPSTORE_PASSWORD: ${{ secrets.APPSTORE_PASSWORD }}
USE_HARD_LINKS: false
# DEBUG: electron-builder,electron-builder:* # DEBUG: electron-builder,electron-builder:*
- name: Build packages without signing - name: Build packages without signing
@@ -61,6 +66,15 @@ jobs:
ARCH: ${{matrix.arch}} ARCH: ${{matrix.arch}}
# DEBUG: electron-builder,electron-builder:* # DEBUG: electron-builder,electron-builder:*
- name: Upload symbols
run: |
sudo npm install -g @sentry/cli --unsafe-perm
./scripts/sentry-upload.js
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
- name: Package artifacts - name: Package artifacts
run: | run: |
mkdir artifact-pkg mkdir artifact-pkg

View File

@@ -11,7 +11,7 @@ jobs:
- name: Installing Node - name: Installing Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 15 node-version: 14
- name: Build - name: Build
shell: powershell shell: powershell
@@ -34,6 +34,15 @@ jobs:
run: node scripts/build-windows.js run: node scripts/build-windows.js
if: github.repository != 'Eugeny/terminus' || github.event_name != 'push' if: github.repository != 'Eugeny/terminus' || github.event_name != 'push'
- name: Upload symbols
run: |
npm install @sentry/cli
node scripts/sentry-upload.js
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
- name: Package artifacts - name: Package artifacts
run: | run: |
mkdir artifact-setup mkdir artifact-setup

View File

@@ -2,11 +2,11 @@
<p align="center"> <p align="center">
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/eugeny/terminus.svg?label=License&style=flat-square"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus"><img alt="AppVeyor" src="https://img.shields.io/appveyor/ci/eugeny/terminus.svg?label=CI&logo=appveyor&logoColor=white&style=flat-square"></a> <a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/eugeny/terminus.svg?label=License&style=flat-square"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus"><img alt="AppVeyor" src="https://img.shields.io/appveyor/ci/eugeny/****terminus****.svg?label=CI&logo=appveyor&logoColor=white&style=flat-square"></a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/Eugeny/terminus/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/terminus/total.svg?label=DOWNLOAD&logo=github&style=for-the-badge"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts"><img src="https://img.shields.io/badge/download-nightly%20build-magenta.svg?logo=appveyor&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=blue&logo=gitter&style=for-the-badge"></a> <a href="https://github.com/Eugeny/terminus/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/terminus/total.svg?label=RELEASE&logo=github&style=for-the-badge"></a> <a href="https://nightly.link/Eugeny/terminus/workflows/windows/master"><img src="https://shields.io/badge/-Nightly-blue?logo=windows&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/terminus/workflows/macos/master"><img src="https://shields.io/badge/-Nightly-black?logo=apple&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/terminus/workflows/linux/master"><img src="https://shields.io/badge/-Nightly-orange?logo=linux&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=magenta&logo=gitter&style=for-the-badge"></a>
</p> </p>
---- ----
@@ -74,57 +74,59 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable --> <!-- 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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=""/><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?s=100" 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/LeSeulArtichaut"><img src="https://avatars1.githubusercontent.com/u/38361244?v=4?s=100" 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> <td align="center"><a href="https://github.com/CyrilTaylor"><img src="https://avatars0.githubusercontent.com/u/12631466?v=4?s=100" 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>
<td align="center"><a href="https://github.com/nstefanou"><img src="https://avatars3.githubusercontent.com/u/51129173?v=4" width="100px;" alt=""/><br /><sub><b>nstefanou</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=nstefanou" title="Code">💻</a> <a href="#plugin-nstefanou" title="Plugin/utility libraries">🔌</a></td> <td align="center"><a href="https://github.com/nstefanou"><img src="https://avatars3.githubusercontent.com/u/51129173?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nstefanou</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=nstefanou" title="Code">💻</a> <a href="#plugin-nstefanou" title="Plugin/utility libraries">🔌</a></td>
<td align="center"><a href="https://github.com/orin220444"><img src="https://avatars3.githubusercontent.com/u/30747229?v=4" width="100px;" alt=""/><br /><sub><b>orin220444</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=orin220444" title="Code">💻</a></td> <td align="center"><a href="https://github.com/orin220444"><img src="https://avatars3.githubusercontent.com/u/30747229?v=4?s=100" width="100px;" alt=""/><br /><sub><b>orin220444</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=orin220444" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/Goobles"><img src="https://avatars3.githubusercontent.com/u/8776771?v=4" width="100px;" alt=""/><br /><sub><b>Gobius Dolhain</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Goobles" title="Code">💻</a></td> <td align="center"><a href="https://github.com/Goobles"><img src="https://avatars3.githubusercontent.com/u/8776771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gobius Dolhain</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Goobles" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/3l0w"><img src="https://avatars2.githubusercontent.com/u/37798980?v=4" width="100px;" alt=""/><br /><sub><b>Gwilherm Folliot</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=3l0w" title="Code">💻</a></td> <td align="center"><a href="https://github.com/3l0w"><img src="https://avatars2.githubusercontent.com/u/37798980?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gwilherm Folliot</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=3l0w" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dimitory"><img src="https://avatars0.githubusercontent.com/u/475955?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Pronin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=dimitory" title="Code">💻</a></td> <td align="center"><a href="https://github.com/Dimitory"><img src="https://avatars0.githubusercontent.com/u/475955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dmitry Pronin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=dimitory" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/JonathanBeverley"><img src="https://avatars1.githubusercontent.com/u/20328966?v=4" width="100px;" alt=""/><br /><sub><b>Jonathan Beverley</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=JonathanBeverley" title="Code">💻</a></td> <td align="center"><a href="https://github.com/JonathanBeverley"><img src="https://avatars1.githubusercontent.com/u/20328966?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Beverley</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=JonathanBeverley" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/zend"><img src="https://avatars1.githubusercontent.com/u/25160?v=4" width="100px;" alt=""/><br /><sub><b>Zenghai Liang</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=zend" title="Code">💻</a></td> <td align="center"><a href="https://github.com/zend"><img src="https://avatars1.githubusercontent.com/u/25160?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zenghai Liang</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=zend" title="Code">💻</a></td>
<td align="center"><a href="https://about.me/matishadow"><img src="https://avatars0.githubusercontent.com/u/9083085?v=4" width="100px;" alt=""/><br /><sub><b>Mateusz Tracz</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=matishadow" title="Code">💻</a></td> <td align="center"><a href="https://about.me/matishadow"><img src="https://avatars0.githubusercontent.com/u/9083085?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mateusz Tracz</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=matishadow" title="Code">💻</a></td>
<td align="center"><a href="https://zergpool.com"><img src="https://avatars3.githubusercontent.com/u/36234677?v=4" width="100px;" alt=""/><br /><sub><b>pinpin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=pinpins" title="Code">💻</a></td> <td align="center"><a href="https://zergpool.com"><img src="https://avatars3.githubusercontent.com/u/36234677?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pinpin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=pinpins" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/TakuroOnoda"><img src="https://avatars0.githubusercontent.com/u/1407926?v=4" width="100px;" alt=""/><br /><sub><b>Takuro Onoda</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=TakuroOnoda" title="Code">💻</a></td> <td align="center"><a href="https://github.com/TakuroOnoda"><img src="https://avatars0.githubusercontent.com/u/1407926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Takuro Onoda</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=TakuroOnoda" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/frauhottelmann"><img src="https://avatars2.githubusercontent.com/u/902705?v=4?s=100" width="100px;" alt=""/><br /><sub><b>frauhottelmann</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=frauhottelmann" title="Code">💻</a></td>
</tr> </tr>
</table> </table>
<!-- markdownlint-enable --> <!-- markdownlint-restore -->
<!-- prettier-ignore-end --> <!-- 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!

View File

@@ -1,9 +1,11 @@
import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron' import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron'
import * as promiseIpc from 'electron-promise-ipc'
import { loadConfig } from './config' import { loadConfig } from './config'
import { Window, WindowOptions } from './window' import { Window, WindowOptions } from './window'
import { pluginManager } from './pluginManager'
export class Application { export class Application {
private tray: Tray private tray?: Tray
private windows: Window[] = [] private windows: Window[] = []
constructor () { constructor () {
@@ -20,6 +22,14 @@ export class Application {
} }
}) })
;(promiseIpc as any).on('plugin-manager:install', (path, name, version) => {
return pluginManager.install(path, name, version)
})
;(promiseIpc as any).on('plugin-manager:uninstall', (path, name) => {
return pluginManager.uninstall(path, name)
})
const configData = loadConfig() const configData = loadConfig()
if (process.platform === 'linux') { if (process.platform === 'linux') {
app.commandLine.appendSwitch('no-sandbox') app.commandLine.appendSwitch('no-sandbox')
@@ -65,7 +75,7 @@ export class Application {
} }
onGlobalHotkey (): void { onGlobalHotkey (): void {
if (this.windows.some(x => x.isFocused())) { if (this.windows.some(x => x.isFocused() && x.isVisible())) {
for (const window of this.windows) { for (const window of this.windows) {
window.hide() window.hide()
} }
@@ -121,10 +131,8 @@ export class Application {
} }
disableTray (): void { disableTray (): void {
if (this.tray) { this.tray?.destroy()
this.tray.destroy() this.tray = null
this.tray = null
}
} }
hasWindows (): boolean { hasWindows (): boolean {
@@ -139,7 +147,7 @@ export class Application {
handleSecondInstance (argv: string[], cwd: string): void { handleSecondInstance (argv: string[], cwd: string): void {
this.presentAllWindows() this.presentAllWindows()
this.windows[this.windows.length - 1].handleSecondInstance(argv, cwd) this.windows[this.windows.length - 1].passCliArguments(argv, cwd, true)
} }
private setupMenu () { private setupMenu () {

View File

@@ -5,7 +5,7 @@ export function parseArgs (argv: string[], cwd: string): any {
argv = argv.slice(1) argv = argv.slice(1)
} }
return require('yargs') return require('yargs/yargs')(argv.slice(1))
.usage('terminus [command] [arguments]') .usage('terminus [command] [arguments]')
.command('open [directory]', 'open a shell in a directory', { .command('open [directory]', 'open a shell in a directory', {
directory: { type: 'string', 'default': cwd }, directory: { type: 'string', 'default': cwd },
@@ -41,5 +41,5 @@ export function parseArgs (argv: string[], cwd: string): any {
type: 'boolean', type: 'boolean',
}) })
.help('help') .help('help')
.parse(argv.slice(1)) .parse()
} }

View File

@@ -6,7 +6,7 @@ import { app } from 'electron'
export function loadConfig (): any { export function loadConfig (): any {
const configPath = path.join(app.getPath('userData'), 'config.yaml') const configPath = path.join(app.getPath('userData'), 'config.yaml')
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
return yaml.safeLoad(fs.readFileSync(configPath, 'utf8')) return yaml.load(fs.readFileSync(configPath, 'utf8'))
} else { } else {
return {} return {}
} }

View File

@@ -1,4 +1,5 @@
import './portable' import './portable'
import 'source-map-support/register'
import './sentry' import './sentry'
import './lru' import './lru'
import { app, ipcMain, Menu } from 'electron' import { app, ipcMain, Menu } from 'electron'
@@ -52,7 +53,7 @@ if (argv.d) {
}) })
} }
app.on('ready', () => { app.on('ready', async () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.dock.setMenu(Menu.buildFromTemplate([ app.dock.setMenu(Menu.buildFromTemplate([
{ {
@@ -64,5 +65,8 @@ app.on('ready', () => {
])) ]))
} }
application.init() application.init()
application.newWindow({ hidden: argv.hidden })
const window = await application.newWindow({ hidden: argv.hidden })
await window.ready
window.passCliArguments(process.argv, process.cwd(), false)
}) })

40
app/lib/pluginManager.ts Normal file
View File

@@ -0,0 +1,40 @@
import { promisify } from 'util'
export class PluginManager {
npm: any
npmReady?: Promise<void>
async ensureLoaded (): Promise<void> {
if (!this.npmReady) {
this.npmReady = new Promise(resolve => {
const npm = require('npm')
npm.load(err => {
if (err) {
console.error(err)
return
}
npm.config.set('global', false)
this.npm = npm
resolve()
})
})
}
return this.npmReady
}
async install (path: string, name: string, version: string): Promise<void> {
await this.ensureLoaded()
this.npm.prefix = path
return promisify(this.npm.commands.install)([`${name}@${version}`])
}
async uninstall (path: string, name: string): Promise<void> {
await this.ensureLoaded()
this.npm.prefix = path
return promisify(this.npm.commands.remove)([name])
}
}
export const pluginManager = new PluginManager()

View File

@@ -8,17 +8,15 @@ try {
appPath = path.dirname(require('electron').remote.app.getPath('exe')) appPath = path.dirname(require('electron').remote.app.getPath('exe'))
} }
if (null != appPath) { if (fs.existsSync(path.join(appPath, 'terminus-data'))) {
if (fs.existsSync(path.join(appPath, 'terminus-data'))) { fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data'))
fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data')) }
} const portableData = path.join(appPath, 'data')
const portableData = path.join(appPath, 'data') if (fs.existsSync(portableData)) {
if (fs.existsSync(portableData)) { console.log('reset user data to ' + portableData)
console.log('reset user data to ' + portableData) try {
try { require('electron').app.setPath('userData', portableData)
require('electron').app.setPath('userData', portableData) } catch {
} catch { require('electron').remote.app.setPath('userData', portableData)
require('electron').remote.app.setPath('userData', portableData)
}
} }
} }

View File

@@ -1,7 +1,4 @@
import * as glasstron from 'glasstron' import * as glasstron from 'glasstron'
if (process.platform === 'win32' || process.platform === 'linux') {
glasstron.init()
}
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { debounceTime } from 'rxjs/operators' import { debounceTime } from 'rxjs/operators'
@@ -9,6 +6,8 @@ import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowCons
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 * as path from 'path'
import macOSRelease from 'macos-release'
import * as compareVersions from 'compare-versions'
import { parseArgs } from './cli' import { parseArgs } from './cli'
import { loadConfig } from './config' import { loadConfig } from './config'
@@ -27,15 +26,17 @@ abstract class GlasstronWindow extends BrowserWindow {
abstract setBlur (_: boolean) abstract setBlur (_: boolean)
} }
const macOSVibrancyType = process.platform === 'darwin' ? compareVersions.compare(macOSRelease().version, '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null
export class Window { export class Window {
ready: Promise<void> ready: Promise<void>
private visible = new Subject<boolean>() private visible = new Subject<boolean>()
private closed = new Subject<void>() private closed = new Subject<void>()
private window: GlasstronWindow private window?: GlasstronWindow
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 lastVibrancy: { enabled: boolean, type?: string } | null = null
private disableVibrancyWhileDragging = false private disableVibrancyWhileDragging = false
private configStore: any private configStore: any
@@ -45,7 +46,7 @@ export class Window {
constructor (options?: WindowOptions) { constructor (options?: WindowOptions) {
this.configStore = loadConfig() this.configStore = loadConfig()
options = options || {} options = options ?? {}
this.windowConfig = new ElectronConfig({ name: 'window' }) this.windowConfig = new ElectronConfig({ name: 'window' })
this.windowBounds = this.windowConfig.get('windowBoundaries') this.windowBounds = this.windowConfig.get('windowBoundaries')
@@ -62,7 +63,9 @@ export class Window {
preload: path.join(__dirname, 'sentry.js'), preload: path.join(__dirname, 'sentry.js'),
backgroundThrottling: false, backgroundThrottling: false,
enableRemoteModule: true, enableRemoteModule: true,
contextIsolation: false,
}, },
maximizable: true,
frame: false, frame: false,
show: false, show: false,
backgroundColor: '#00000000', backgroundColor: '#00000000',
@@ -89,11 +92,15 @@ export class Window {
} }
} }
this.window = new glasstron.BrowserWindow(bwOptions) if (process.platform === 'darwin') {
this.window = new BrowserWindow(bwOptions) as GlasstronWindow
} else {
this.window = new glasstron.BrowserWindow(bwOptions)
}
this.window.once('ready-to-show', () => { this.window.once('ready-to-show', () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
this.window.setVibrancy('window') this.window.setVibrancy(macOSVibrancyType)
} else if (process.platform === 'win32' && (this.configStore.appearance || {}).vibrancy) { } else if (process.platform === 'win32' && (this.configStore.appearance || {}).vibrancy) {
this.setVibrancy(true) this.setVibrancy(true)
} }
@@ -141,7 +148,11 @@ export class Window {
if (process.platform === 'win32') { if (process.platform === 'win32') {
if (parseFloat(os.release()) >= 10) { if (parseFloat(os.release()) >= 10) {
this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null
this.window.setBlur(enabled) try {
this.window.setBlur(enabled)
} catch (error) {
console.error('Failed to set window blur', error)
}
} else { } else {
DwmEnableBlurBehindWindow(this.window, enabled) DwmEnableBlurBehindWindow(this.window, enabled)
} }
@@ -149,7 +160,7 @@ export class Window {
this.window.setBackgroundColor(enabled ? '#00000000' : '#131d27') this.window.setBackgroundColor(enabled ? '#00000000' : '#131d27')
this.window.setBlur(enabled) this.window.setBlur(enabled)
} else { } else {
this.window.setVibrancy(enabled ? 'dark' : null as any) // electron issue 20269 this.window.setVibrancy(enabled ? macOSVibrancyType : null)
} }
} }
@@ -180,6 +191,10 @@ export class Window {
return this.window.isFocused() return this.window.isFocused()
} }
isVisible (): boolean {
return this.window.isVisible()
}
hide (): void { hide (): void {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// Lose focus // Lose focus
@@ -215,8 +230,8 @@ export class Window {
} }
} }
handleSecondInstance (argv: string[], cwd: string): void { passCliArguments (argv: string[], cwd: string, secondInstance: boolean): void {
this.send('host:second-instance', parseArgs(argv, cwd), cwd) this.send('cli', parseArgs(argv, cwd), cwd, secondInstance)
} }
private setupWindowManagement () { private setupWindowManagement () {

View File

@@ -1,6 +1,7 @@
{ {
"name": "terminus", "name": "terminus",
"description": "A terminal for a modern age", "description": "A terminal for a modern age",
"private": true,
"repository": "https://github.com/eugeny/terminus", "repository": "https://github.com/eugeny/terminus",
"author": { "author": {
"name": "Eugene Pankov", "name": "Eugene Pankov",
@@ -13,28 +14,29 @@
"watch": "webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^9.1.9", "@angular/animations": "^11.1.1",
"@angular/common": "^9.1.11", "@angular/common": "^11.1.1",
"@angular/compiler": "^9.1.9", "@angular/compiler": "^11.1.1",
"@angular/core": "^9.1.9", "@angular/core": "^11.1.1",
"@angular/forms": "^9.1.11", "@angular/forms": "^11.1.1",
"@angular/platform-browser": "^9.1.9", "@angular/platform-browser": "^11.1.1",
"@angular/platform-browser-dynamic": "^9.1.9", "@angular/platform-browser-dynamic": "^11.1.1",
"@ng-bootstrap/ng-bootstrap": "^6.1.0", "@ng-bootstrap/ng-bootstrap": "^6.1.0",
"@terminus-term/node-pty": "0.10.0-beta10", "@terminus-term/node-pty": "0.10.0-terminus.2",
"electron-config": "2.0.0", "electron-config": "2.0.0",
"electron-debug": "^3.0.1", "electron-debug": "^3.2.0",
"electron-is-dev": "1.1.0", "electron-is-dev": "1.2.0",
"electron-promise-ipc": "^2.2.4",
"fontmanager-redux": "1.0.0", "fontmanager-redux": "1.0.0",
"glasstron": "0.0.5", "glasstron": "0.0.6",
"js-yaml": "3.14.0", "js-yaml": "4.0.0",
"keytar": "^7.2.0", "keytar": "^7.2.0",
"mz": "^2.7.0", "mz": "^2.7.0",
"native-process-working-directory": "^1.0.2",
"ngx-toastr": "^12.0.1", "ngx-toastr": "^12.0.1",
"npm": "7.0.15", "npm": "6",
"path": "0.12.7", "path": "0.12.7",
"rxjs": "^6.5.5", "rxjs": "^6.5.5",
"rxjs-compat": "^6.6.0",
"yargs": "^15.4.1", "yargs": "^15.4.1",
"zone.js": "^0.11.3" "zone.js": "^0.11.3"
}, },
@@ -48,7 +50,8 @@
"devDependencies": { "devDependencies": {
"@types/mz": "0.0.32", "@types/mz": "0.0.32",
"@types/node": "14.14.14", "@types/node": "14.14.14",
"node-abi": "2.19.3" "node-abi": "2.19.3",
"source-map-support": "^0.5.19"
}, },
"peerDependencies": { "peerDependencies": {
"terminus-community-color-schemes": "*", "terminus-community-color-schemes": "*",

View File

@@ -0,0 +1,12 @@
diff --git a/node_modules/node-abi/abi_registry.json b/node_modules/node-abi/abi_registry.json
index bc1436d..630c1b7 100644
--- a/node_modules/node-abi/abi_registry.json
+++ b/node_modules/node-abi/abi_registry.json
@@ -94,6 +94,6 @@
"future": true,
"lts": false,
"runtime": "electron",
- "target": "12.0.0-nightly.20201013"
+ "target": "12.0.0-beta.16"
}
]

View File

@@ -58,8 +58,8 @@ findPlugins().then(async plugins => {
window['safeModeReason'] = error window['safeModeReason'] = error
try { try {
await bootstrap(plugins, true) await bootstrap(plugins, true)
} catch (error) { } catch (error2) {
console.error('Bootstrap failed:', error) console.error('Bootstrap failed:', error2)
} }
} }
}) })

View File

@@ -95,3 +95,7 @@ input[type=range] {
&::-moz-range-track { @include track(); } &::-moz-range-track { @include track(); }
&::-ms-track { @include track(); } &::-ms-track { @include track(); }
} }
a[ngbdropdownitem] {
cursor: pointer;
}

View File

@@ -3,13 +3,13 @@ import * as path from 'path'
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
const nodeRequire = (global as any).require const nodeRequire = (global as any).require
function normalizePath (path: string): string { function normalizePath (p: string): string {
const cygwinPrefix = '/cygdrive/' const cygwinPrefix = '/cygdrive/'
if (path.startsWith(cygwinPrefix)) { if (p.startsWith(cygwinPrefix)) {
path = path.substring(cygwinPrefix.length).replace('/', '\\') p = p.substring(cygwinPrefix.length).replace('/', '\\')
path = path[0] + ':' + path.substring(1) p = p[0] + ':' + p.substring(1)
} }
return path return p
} }
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x))) global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
@@ -63,7 +63,6 @@ const builtinModules = [
'ngx-toastr', 'ngx-toastr',
'rxjs', 'rxjs',
'rxjs/operators', 'rxjs/operators',
'rxjs-compat/Subject',
'terminus-core', 'terminus-core',
'terminus-settings', 'terminus-settings',
'terminus-terminal', 'terminus-terminal',

View File

@@ -35,12 +35,15 @@ module.exports = {
externals: { externals: {
electron: 'commonjs electron', electron: 'commonjs electron',
'electron-config': 'commonjs electron-config', 'electron-config': 'commonjs electron-config',
'electron-promise-ipc': 'commonjs electron-promise-ipc',
'electron-vibrancy': 'commonjs electron-vibrancy', 'electron-vibrancy': 'commonjs electron-vibrancy',
fs: 'commonjs fs', fs: 'commonjs fs',
glasstron: 'commonjs glasstron', glasstron: 'commonjs glasstron',
mz: 'commonjs mz', mz: 'commonjs mz',
npm: 'commonjs npm',
path: 'commonjs path', path: 'commonjs path',
yargs: 'commonjs yargs', util: 'commonjs util',
'source-map-support': 'commonjs source-map-support',
'windows-swca': 'commonjs windows-swca', 'windows-swca': 'commonjs windows-swca',
'windows-blurbehind': 'commonjs windows-blurbehind', 'windows-blurbehind': 'commonjs windows-blurbehind',
}, },
@@ -50,4 +53,6 @@ module.exports = {
'process.type': '"main"', 'process.type': '"main"',
}), }),
], ],
// Ignore warnings due to yarg's dynamic module loading
ignoreWarnings: [/node_modules\/yargs/],
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,28 @@ npmRebuild: false
afterSign: "./build/mac/afterSignHook.js" afterSign: "./build/mac/afterSignHook.js"
afterAllArtifactBuild: "./build/mac/afterBuildHook.js" afterAllArtifactBuild: "./build/mac/afterBuildHook.js"
files: files:
- "**/*" - '**/*'
- dist - dist
- '!lib'
- '!src'
- '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,node.lib}'
- '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples,docs}'
- '!**/node_modules/@angular/common/locales'
- '!**/node_modules/@angular/compiler/src'
- '!**/node_modules/node-gyp'
- '!**/node_modules/**/*.d.ts'
- '!**/node_modules/**/*.map'
- '!**/node_modules/**/include/node'
- '!**/node_modules/.bin'
- '!**/node_modules/*/*/{esm5,fesm5,esm2015,fesm2015,_esm2015,_fesm2015}'
- '!**/*.{woff,ttf,otf,eot}'
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
- '!.editorconfig'
- '!**/._*'
- '!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}'
- '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}'
- '!**/{appveyor.yml,.travis.yml,circle.yml}'
- '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json'
extraResources: extraResources:
- builtin-plugins - builtin-plugins
- extras - extras
@@ -22,6 +42,7 @@ nsis:
oneClick: false oneClick: false
artifactName: terminus-${version}-setup.${ext} artifactName: terminus-${version}-setup.${ext}
installerIcon: "./build/windows/icon.ico" installerIcon: "./build/windows/icon.ico"
allowToChangeInstallationDirectory: true
mac: mac:
category: public.app-category.video category: public.app-category.video

View File

@@ -2,59 +2,62 @@
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"@sentry/cli": "^1.61.0", "@sentry/cli": "^1.61.0",
"@sentry/electron": "^2.0.4", "@sentry/electron": "^2.2.0",
"@terminus-term/to-string-loader": "1.1.7-beta.1",
"@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/fs-extra": "^8.1.1", "@types/fs-extra": "^8.1.1",
"@types/js-yaml": "^3.12.5", "@types/js-yaml": "^3.12.5",
"@types/node": "14.14.14", "@types/node": "14.14.14",
"@types/webpack-env": "^1.16.0", "@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.11.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.11.0", "@typescript-eslint/parser": "^4.14.1",
"apply-loader": "2.0.0", "apply-loader": "2.0.0",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
"core-js": "^3.8.1", "compare-versions": "^3.6.0",
"cross-env": "7.0.2", "core-js": "^3.8.3",
"css-loader": "3.4.2", "cross-env": "7.0.3",
"electron": "^11.1.1", "css-loader": "5.0.1",
"electron-builder": "22.10.3", "electron": "12.0.0-beta.22",
"electron-builder": "22.10.4",
"electron-download": "^4.1.1", "electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0", "electron-installer-snap": "^5.1.0",
"electron-notarize": "^1.0.0", "electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.4", "electron-rebuild": "^2.3.4",
"eslint": "^7.6.0", "eslint": "^7.18.0",
"eslint-plugin-import": "^2.21.1", "eslint-plugin-import": "^2.21.1",
"file-loader": "^5.1.0", "file-loader": "^6.2.0",
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
"html-loader": "0.5.5", "html-loader": "1.3.2",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"macos-release": "^2.4.1",
"node-abi": "^2.19.3", "node-abi": "^2.19.3",
"node-gyp": "^7.1.2", "node-gyp": "^7.1.2",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"npmlog": "4.1.2", "npmlog": "4.1.2",
"npx": "^10.2.2", "npx": "^10.2.2",
"pug": "^2.0.4", "patch-package": "^6.2.2",
"pug": "^3.0.0",
"pug-html-loader": "1.1.5", "pug-html-loader": "1.1.5",
"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",
"raw-loader": "4.0.1", "raw-loader": "4.0.2",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.1",
"shelljs": "0.8.4", "shelljs": "0.8.4",
"source-code-pro": "^2.30.2", "source-code-pro": "^2.30.2",
"source-sans-pro": "3.6.0", "source-sans-pro": "3.6.0",
"ssh2-streams": "^0.4.10", "ssh2-streams": "^0.4.10",
"style-loader": "^1.3.0", "style-loader": "^2.0.0",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"to-string-loader": "1.1.6",
"tslib": "^2.0.3", "tslib": "^2.0.3",
"typedoc": "^0.18.0", "typedoc": "^0.18.0",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"url-loader": "^3.0.0", "url-loader": "^4.1.1",
"val-loader": "2.1.1", "val-loader": "3.0.0",
"webpack": "^5.11.0", "webpack": "^5.18.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.4.0",
"yaml-loader": "0.6.0" "yaml-loader": "0.6.0"
}, },
"resolutions": { "resolutions": {
@@ -72,7 +75,5 @@
"lint": "eslint --ext ts */src */lib", "lint": "eslint --ext ts */src */lib",
"postinstall": "node ./scripts/install-deps.js" "postinstall": "node ./scripts/install-deps.js"
}, },
"repository": "eugeny/terminus", "private": true
"author": "Eugene Pankov",
"license": "MIT"
} }

View File

@@ -0,0 +1,12 @@
diff --git a/node_modules/node-abi/abi_registry.json b/node_modules/node-abi/abi_registry.json
index bc1436d..630c1b7 100644
--- a/node_modules/node-abi/abi_registry.json
+++ b/node_modules/node-abi/abi_registry.json
@@ -94,6 +94,6 @@
"future": true,
"lts": false,
"runtime": "electron",
- "target": "12.0.0-nightly.20201013"
+ "target": "12.0.0-beta.16"
}
]

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const builder = require('electron-builder').build const builder = require('electron-builder').build
const vars = require('./vars') const vars = require('./vars')
const fs = require('fs')
const signHook = require('../build/mac/afterSignHook')
const isTag = (process.env.GITHUB_REF || '').startsWith('refs/tags/') const isTag = (process.env.GITHUB_REF || '').startsWith('refs/tags/')
@@ -16,6 +14,7 @@ builder({
extraMetadata: { extraMetadata: {
version: vars.version, version: vars.version,
}, },
npmRebuild: process.env.ARCH !== 'arm64',
}, },
publish: isTag ? 'always' : 'onTag', publish: isTag ? 'always' : 'onTag',
}).catch(e => { }).catch(e => {

24
scripts/sentry-upload.js Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
const sh = require('shelljs')
const vars = require('./vars')
const sentryCli = process.platform === 'win32' ? 'node_modules\\.bin\\sentry-cli.cmd' : 'sentry-cli'
sh.exec(`${sentryCli} releases new ${vars.version}`)
if (process.platform === 'darwin') {
for (const path of [
'app/node_modules/@serialport/bindings/build/Release/bindings.node',
'app/node_modules/@terminus-term/node-pty/build/Release/pty.node',
'app/node_modules/fontmanager-redux/build/Release/fontmanager.node',
'app/node_modules/macos-native-processlist/build/Release/native.node',
]) {
sh.exec('dsymutil ' + path)
}
}
sh.exec(`${sentryCli} upload-dif app/node_modules`)
sh.exec(`${sentryCli} releases set-commits --auto ${vars.version}`)
for (const p of vars.builtinPlugins) {
sh.exec(`${sentryCli} releases files ${vars.version} upload-sourcemaps ${p}/dist -u ${p}/dist/ -d ${process.platform}-${p}`)
}

View File

@@ -1,14 +1,14 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { TerminalColorSchemeProvider, TerminalColorScheme } from 'terminus-terminal' import { TerminalColorSchemeProvider, TerminalColorScheme } from 'terminus-terminal'
const schemeContents = require.context('../schemes/', true, /.*/) const schemeContents = require.context('../schemes/', false, /.*/)
@Injectable() @Injectable()
export class ColorSchemes extends TerminalColorSchemeProvider { export class ColorSchemes extends TerminalColorSchemeProvider {
async getSchemes (): Promise<TerminalColorScheme[]> { async getSchemes (): Promise<TerminalColorScheme[]> {
const schemes: TerminalColorScheme[] = [] const schemes: TerminalColorScheme[] = []
schemeContents.keys().forEach(schemeFile => { schemeContents.keys().filter(x => !x.startsWith('./')).forEach(schemeFile => {
const lines = (schemeContents(schemeFile).default as string).split('\n') const lines = (schemeContents(schemeFile).default as string).split('\n')
// process #define variables // process #define variables

View File

@@ -8,7 +8,5 @@ export interface HotkeyDescription {
* must also provide the `hotkeys.foo` config options with the default values * must also provide the `hotkeys.foo` config options with the default values
*/ */
export abstract class HotkeyProvider { export abstract class HotkeyProvider {
hotkeys: HotkeyDescription[] = []
abstract provide (): Promise<HotkeyDescription[]> abstract provide (): Promise<HotkeyDescription[]>
} }

View File

@@ -26,8 +26,8 @@ $side-tab-width: 200px;
.content { .content {
width: 100vw; width: 100vw;
height: 100vh; flex: 1 1 0;
flex: auto; min-height: 0;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
@@ -66,6 +66,10 @@ $side-tab-width: 200px;
.drag-space { .drag-space {
flex: auto; flex: auto;
} }
&>.inset {
opacity: 0;
}
} }
@@ -92,7 +96,7 @@ $side-tab-width: 200px;
color: #aaa; color: #aaa;
border: none; border: none;
border-radius: 0; border-radius: 0;
align-items: center; align-items: center;
&.dropdown-toggle::after { &.dropdown-toggle::after {
@@ -121,7 +125,6 @@ $side-tab-width: 200px;
width: 85px; width: 85px;
height: $tabs-height; height: $tabs-height;
flex: none; flex: none;
opacity: 0;
-webkit-app-region: drag; -webkit-app-region: drag;
} }

View File

@@ -142,6 +142,8 @@ export class AppRootComponent {
this.touchbar.update() this.touchbar.update()
this.hostApp.useBuiltinGraphics()
config.changed$.subscribe(() => this.updateVibrancy()) config.changed$.subscribe(() => this.updateVibrancy())
this.updateVibrancy() this.updateVibrancy()
@@ -229,8 +231,8 @@ export class AppRootComponent {
buttons = buttons.concat(provider.provide()) buttons = buttons.concat(provider.provide())
}) })
return buttons return buttons
.filter(button => (button.weight || 0) > 0 === aboveZero) .filter(button => (button.weight ?? 0) > 0 === aboveZero)
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight || 0) - (b.weight || 0)) .sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
} }
private updateVibrancy () { private updateVibrancy () {

View File

@@ -1,4 +1,4 @@
import { Component, Input, HostListener, ViewChildren, QueryList, ElementRef } from '@angular/core' import { Component, Input, HostListener, ViewChildren, QueryList, ElementRef } from '@angular/core' // eslint-disable-line @typescript-eslint/no-unused-vars
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SelectorOption } from '../api/selector' import { SelectorOption } from '../api/selector'
@@ -50,7 +50,7 @@ export class SelectorModalComponent<T> {
this.filteredOptions = this.options.filter(x => !x.freeInputPattern) this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
} else { } else {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
this.filteredOptions = this.options.filter(x => x.freeInputPattern || (x.name + (x.description || '')).toLowerCase().includes(f)) this.filteredOptions = this.options.filter(x => x.freeInputPattern ?? (x.name + (x.description ?? '')).toLowerCase().includes(f))
} }
this.selectedIndex = Math.max(0, this.selectedIndex) this.selectedIndex = Math.max(0, this.selectedIndex)
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex) this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)
@@ -72,7 +72,7 @@ export class SelectorModalComponent<T> {
this.modalInstance.dismiss() this.modalInstance.dismiss()
} }
iconIsSVG (icon: string): boolean { iconIsSVG (icon?: string): boolean {
return icon?.startsWith('<') return icon?.startsWith('<') ?? false
} }
} }

View File

@@ -161,7 +161,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
_allFocusMode = false _allFocusMode = false
/** @hidden */ /** @hidden */
private focusedTab: BaseTabComponent private focusedTab: BaseTabComponent|null = null
private maximizedTab: BaseTabComponent|null = null 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()
@@ -211,7 +211,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred())) this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) { if (!this.hasFocus || !this.focusedTab) {
return return
} }
switch (hotkey) { switch (hotkey) {
@@ -280,7 +280,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
return this.root.getAllTabs() return this.root.getAllTabs()
} }
getFocusedTab (): BaseTabComponent { getFocusedTab (): BaseTabComponent|null {
return this.focusedTab return this.focusedTab
} }
@@ -295,10 +295,8 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
x.emitBlurred() x.emitBlurred()
} }
} }
if (tab) { tab.emitFocused()
tab.emitFocused() this.focusChanged.next(tab)
this.focusChanged.next(tab)
}
if (this.maximizedTab !== tab) { if (this.maximizedTab !== tab) {
this.maximizedTab = null this.maximizedTab = null
@@ -314,7 +312,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** /**
* Focuses the first available tab inside the given [[SplitContainer]] * Focuses the first available tab inside the given [[SplitContainer]]
*/ */
focusAnyIn (parent: BaseTabComponent | SplitContainer): void { focusAnyIn (parent?: BaseTabComponent | SplitContainer): void {
if (!parent) { if (!parent) {
return return
} }
@@ -331,7 +329,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> { async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
tab.parent = this tab.parent = this
let target = (relative ? this.getParentOf(relative) : null) || this.root let target = (relative ? this.getParentOf(relative) : null) ?? this.root
let insertIndex = relative ? target.children.indexOf(relative) : -1 let insertIndex = relative ? target.children.indexOf(relative) : -1
if ( if (
@@ -398,6 +396,10 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
* Moves focus in the given direction * Moves focus in the given direction
*/ */
navigate (dir: SplitDirection): void { navigate (dir: SplitDirection): void {
if (!this.focusedTab) {
return
}
let rel: BaseTabComponent | SplitContainer = this.focusedTab let rel: BaseTabComponent | SplitContainer = this.focusedTab
let parent = this.getParentOf(rel) let parent = this.getParentOf(rel)
if (!parent) { if (!parent) {
@@ -442,7 +444,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
* @returns the immediate parent of `tab` * @returns the immediate parent of `tab`
*/ */
getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer|null { getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer|null {
root = root || this.root root = root ?? this.root
for (const child of root.children) { for (const child of root.children) {
if (child instanceof SplitContainer) { if (child instanceof SplitContainer) {
const r = this.getParentOf(tab, child) const r = this.getParentOf(tab, child)
@@ -469,7 +471,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */ /** @hidden */
async getCurrentProcess (): Promise<BaseTabProcess|null> { async getCurrentProcess (): Promise<BaseTabProcess|null> {
return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x) || null return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x) ?? null
} }
/** @hidden */ /** @hidden */
@@ -518,7 +520,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) { private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
const size = root.orientation === 'v' ? h : w const size = root.orientation === 'v' ? h : w
const sizes = root.ratios.map(x => x * size) const sizes = root.ratios.map(ratio => ratio * size)
root.x = x root.x = x
root.y = y root.y = y
@@ -598,7 +600,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
@Injectable() @Injectable()
export class SplitTabRecoveryProvider extends TabRecoveryProvider { export class SplitTabRecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> { async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
if (recoveryToken && recoveryToken.type === 'app:split-tab') { if (recoveryToken.type === 'app:split-tab') {
return { return {
type: SplitTabComponent, type: SplitTabComponent,
options: { _recoveredState: recoveryToken }, options: { _recoveredState: recoveryToken },

View File

@@ -34,8 +34,8 @@ export class SplitTabSpannerComponent {
let current = start let current = start
const oldPosition: number = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft const oldPosition: number = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft
const dragHandler = (e: MouseEvent) => { const dragHandler = (dragEvent: MouseEvent) => {
current = this.isVertical ? e.pageY : e.pageX current = this.isVertical ? dragEvent.pageY : dragEvent.pageX
const newPosition = oldPosition + (current - start) const newPosition = oldPosition + (current - start)
if (this.isVertical) { if (this.isVertical) {
this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px` this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px`

View File

@@ -26,10 +26,10 @@ export class StartPageComponent {
.map(provider => provider.provide()) .map(provider => provider.provide())
.reduce((a, b) => a.concat(b)) .reduce((a, b) => a.concat(b))
.filter(x => !!x.click) .filter(x => !!x.click)
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight || 0) - (b.weight || 0)) .sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
} }
sanitizeIcon (icon: string): any { sanitizeIcon (icon?: string): any {
return this.domSanitizer.bypassSecurityTrustHtml(icon || '') return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
} }
} }

View File

@@ -110,7 +110,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
}) })
} }
static forRoot (): ModuleWithProviders { static forRoot (): ModuleWithProviders<AppModule> {
return { return {
ngModule: AppModule, ngModule: AppModule,
providers: PROVIDERS, providers: PROVIDERS,

View File

@@ -46,13 +46,13 @@ class CompletionObserver {
export class AppService { export class AppService {
tabs: BaseTabComponent[] = [] tabs: BaseTabComponent[] = []
get activeTab (): BaseTabComponent { return this._activeTab } get activeTab (): BaseTabComponent|null { return this._activeTab ?? null }
private lastTabIndex = 0 private lastTabIndex = 0
private _activeTab: BaseTabComponent private _activeTab: BaseTabComponent | null = null
private closedTabsStack: RecoveryToken[] = [] private closedTabsStack: RecoveryToken[] = []
private activeTabChange = new Subject<BaseTabComponent>() private activeTabChange = new Subject<BaseTabComponent|null>()
private tabsChanged = new Subject<void>() private tabsChanged = new Subject<void>()
private tabOpened = new Subject<BaseTabComponent>() private tabOpened = new Subject<BaseTabComponent>()
private tabClosed = new Subject<BaseTabComponent>() private tabClosed = new Subject<BaseTabComponent>()
@@ -60,7 +60,7 @@ export class AppService {
private completionObservers = new Map<BaseTabComponent, CompletionObserver>() private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
get activeTabChange$ (): Observable<BaseTabComponent> { return this.activeTabChange } get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened } get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
get tabsChanged$ (): Observable<void> { return this.tabsChanged } get tabsChanged$ (): Observable<void> { return this.tabsChanged }
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed } get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
@@ -97,9 +97,7 @@ export class AppService {
} }
} }
hostApp.windowFocused$.subscribe(() => { hostApp.windowFocused$.subscribe(() => this._activeTab?.emitFocused())
this._activeTab?.emitFocused()
})
this.tabClosed$.subscribe(async tab => { this.tabClosed$.subscribe(async tab => {
const token = await tab.getRecoveryToken() const token = await tab.getRecoveryToken()
@@ -187,12 +185,12 @@ export class AppService {
return null return null
} }
selectTab (tab: BaseTabComponent): void { selectTab (tab: BaseTabComponent|null): void {
if (this._activeTab === tab) { if (tab && this._activeTab === tab) {
this._activeTab.emitFocused() this._activeTab.emitFocused()
return return
} }
if (this.tabs.includes(this._activeTab)) { if (this._activeTab && this.tabs.includes(this._activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this._activeTab) this.lastTabIndex = this.tabs.indexOf(this._activeTab)
} else { } else {
this.lastTabIndex = 0 this.lastTabIndex = 0
@@ -203,12 +201,10 @@ export class AppService {
} }
this._activeTab = tab this._activeTab = tab
this.activeTabChange.next(tab) this.activeTabChange.next(tab)
if (this._activeTab) { setImmediate(() => {
setImmediate(() => { this._activeTab?.emitFocused()
this._activeTab.emitFocused() })
}) this.hostApp.setTitle(this._activeTab?.title)
this.hostApp.setTitle(this._activeTab.title)
}
} }
getParentTab (tab: BaseTabComponent): SplitTabComponent|null { getParentTab (tab: BaseTabComponent): SplitTabComponent|null {
@@ -231,6 +227,9 @@ export class AppService {
} }
nextTab (): void { nextTab (): void {
if (!this._activeTab) {
return
}
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) { if (tabIndex < this.tabs.length - 1) {
@@ -242,6 +241,9 @@ export class AppService {
} }
previousTab (): void { previousTab (): void {
if (!this._activeTab) {
return
}
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) { if (tabIndex > 0) {
@@ -253,6 +255,9 @@ export class AppService {
} }
moveSelectedTabLeft (): void { moveSelectedTabLeft (): void {
if (!this._activeTab) {
return
}
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) { if (tabIndex > 0) {
@@ -264,6 +269,9 @@ export class AppService {
} }
moveSelectedTabRight (): void { moveSelectedTabRight (): void {
if (!this._activeTab) {
return
}
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) { if (tabIndex < this.tabs.length - 1) {

View File

@@ -109,10 +109,7 @@ export class ConfigService {
) { ) {
this.path = path.join(electron.app.getPath('userData'), 'config.yaml') this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
this.defaults = configProviders.map(provider => { this.defaults = configProviders.map(provider => {
let defaults = {} let defaults = provider.platformDefaults[hostApp.platform] || {}
if (provider.platformDefaults) {
defaults = configMerge(defaults, provider.platformDefaults[hostApp.platform] || {})
}
if (provider.defaults) { if (provider.defaults) {
defaults = configMerge(defaults, provider.defaults) defaults = configMerge(defaults, provider.defaults)
} }
@@ -147,7 +144,7 @@ export class ConfigService {
load (): void { load (): void {
if (fs.existsSync(this.path)) { if (fs.existsSync(this.path)) {
this._store = yaml.safeLoad(fs.readFileSync(this.path, 'utf8')) this._store = yaml.load(fs.readFileSync(this.path, 'utf8'))
} else { } else {
this._store = {} this._store = {}
} }
@@ -157,7 +154,7 @@ export class ConfigService {
save (): void { save (): void {
// Scrub undefined values // Scrub undefined values
this._store = JSON.parse(JSON.stringify(this._store)) this._store = JSON.parse(JSON.stringify(this._store))
fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8') fs.writeFileSync(this.path, yaml.dump(this._store), 'utf8')
this.emitChange() this.emitChange()
this.hostApp.broadcastConfigChange(JSON.parse(JSON.stringify(this.store))) this.hostApp.broadcastConfigChange(JSON.parse(JSON.stringify(this.store)))
} }
@@ -166,14 +163,14 @@ export class ConfigService {
* Reads config YAML as string * Reads config YAML as string
*/ */
readRaw (): string { readRaw (): string {
return yaml.safeDump(this._store) return yaml.dump(this._store)
} }
/** /**
* Writes config YAML as string * Writes config YAML as string
*/ */
writeRaw (data: string): void { writeRaw (data: string): void {
this._store = yaml.safeLoad(data) this._store = yaml.load(data)
this.save() this.save()
this.load() this.load()
this.emitChange() this.emitChange()

View File

@@ -26,6 +26,7 @@ export class DockingService {
let display = this.electron.screen.getAllDisplays() let display = this.electron.screen.getAllDisplays()
.filter(x => x.id === this.config.store.appearance.dockScreen)[0] .filter(x => x.id === this.config.store.appearance.dockScreen)[0]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!display) { if (!display) {
display = this.getCurrentScreen() display = this.getCurrentScreen()
} }

View File

@@ -1,5 +1,6 @@
import type { BrowserWindow, TouchBar, MenuItemConstructorOptions } from 'electron' import type { BrowserWindow, TouchBar, MenuItemConstructorOptions } from 'electron'
import * as path from 'path' import * as path from 'path'
import * as fs from 'mz/fs'
import shellEscape from 'shell-escape' import shellEscape from 'shell-escape'
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { Injectable, NgZone, EventEmitter } from '@angular/core' import { Injectable, NgZone, EventEmitter } from '@angular/core'
@@ -7,6 +8,12 @@ import { ElectronService } from './electron.service'
import { Logger, LogService } from './log.service' import { Logger, LogService } from './log.service'
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils' import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch (_) { }
export enum Platform { export enum Platform {
Linux = 'Linux', Linux = 'Linux',
macOS = 'macOS', macOS = 'macOS',
@@ -150,9 +157,10 @@ export class HostAppService {
this.zone.run(() => this.displaysChanged.next()) this.zone.run(() => this.displaysChanged.next())
}) })
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => { electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => {
this.logger.info('Second instance', argv) this.logger.info('Second instance', argv)
const op = argv._[0] const op = argv._[0]
const opAsPath = op ? path.resolve(cwd, op) : null
if (op === 'open') { if (op === 'open') {
this.cliOpenDirectory.next(path.resolve(cwd, argv.directory)) this.cliOpenDirectory.next(path.resolve(cwd, argv.directory))
} else if (op === 'run') { } else if (op === 'run') {
@@ -165,9 +173,13 @@ export class HostAppService {
this.cliPaste.next(text) this.cliPaste.next(text)
} else if (op === 'profile') { } else if (op === 'profile') {
this.cliOpenProfile.next(argv.profileName) this.cliOpenProfile.next(argv.profileName)
} else if (op === undefined) { } else if (secondInstance && op === undefined) {
this.newWindow() this.newWindow()
} else { } else if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) {
this.cliOpenDirectory.next(opAsPath)
}
if (secondInstance) {
this.secondInstance.next() this.secondInstance.next()
} }
})) }))
@@ -235,15 +247,15 @@ 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|null): void { setVibrancy (enable: boolean, type: string|null): void {
if (!isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) { if (this.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
type = null type = null
} }
document.body.classList.toggle('vibrant', enable) document.body.classList.toggle('vibrant', enable)
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type) this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
} }
setTitle (title: string): void { setTitle (title?: string): void {
this.electron.ipcRenderer.send('window-set-title', title) this.electron.ipcRenderer.send('window-set-title', title ?? 'Terminus')
} }
setTouchBar (touchBar: TouchBar): void { setTouchBar (touchBar: TouchBar): void {
@@ -277,6 +289,16 @@ export class HostAppService {
this.electron.ipcRenderer.send('app:register-global-hotkey', specs) this.electron.ipcRenderer.send('app:register-global-hotkey', specs)
} }
useBuiltinGraphics (): void {
const keyPath = 'SOFTWARE\\Microsoft\\DirectX\\UserGpuPreferences'
const valueName = this.electron.app.getPath('exe')
if (this.platform === Platform.Windows) {
if (!wnr.getRegistryValue(wnr.HK.CU, keyPath, valueName)) {
wnr.setRegistryValue(wnr.HK.CU, keyPath, valueName, wnr.REG.SZ, 'GpuPreference=1;')
}
}
}
relaunch (): void { relaunch (): void {
if (this.isPortable) { if (this.isPortable) {
this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE }) this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE })

View File

@@ -172,7 +172,7 @@ export class HotkeysService {
return ( return (
await Promise.all( await Promise.all(
this.config.enabledServices(this.hotkeyProviders) this.config.enabledServices(this.hotkeyProviders)
.map(async x => x.provide ? x.provide() : x.hotkeys) .map(async x => x.provide())
) )
).reduce((a, b) => a.concat(b)) ).reduce((a, b) => a.concat(b))
} }
@@ -222,7 +222,7 @@ export class HotkeysService {
if (!(value instanceof Array)) { if (!(value instanceof Array)) {
continue continue
} }
if (value) { if (value.length > 0) {
value = value.map((item: string | string[]) => typeof item === 'string' ? [item] : item) value = value.map((item: string | string[]) => typeof item === 'string' ? [item] : item)
keys[key] = value keys[key] = value
} }

View File

@@ -54,9 +54,7 @@ export class Logger {
private doLog (level: string, ...args: any[]): void { private doLog (level: string, ...args: any[]): void {
console[level](`%c[${this.name}]`, 'color: #aaa', ...args) console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
if (this.winstonLogger) { this.winstonLogger[level](...args)
this.winstonLogger[level](...args)
}
} }
} }

View File

@@ -59,7 +59,7 @@ export class ShellIntegrationService {
} }
async install (): Promise<void> { async install (): Promise<void> {
const exe: string = process.env.PORTABLE_EXECUTABLE_FILE || this.electron.app.getPath('exe') const exe: string = process.env.PORTABLE_EXECUTABLE_FILE ?? this.electron.app.getPath('exe')
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
for (const wf of this.automatorWorkflows) { for (const wf of this.automatorWorkflows) {
await exec(`cp -r "${this.automatorWorkflowsLocation}/${wf}" "${this.automatorWorkflowsDestination}"`) await exec(`cp -r "${this.automatorWorkflowsLocation}/${wf}" "${this.automatorWorkflowsDestination}"`)

View File

@@ -11,7 +11,7 @@ export class TabRecoveryService {
enabled = false enabled = false
private constructor ( private constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], @Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[]|null,
private config: ConfigService, private config: ConfigService,
log: LogService log: LogService
) { ) {
@@ -23,35 +23,28 @@ export class TabRecoveryService {
return return
} }
window.localStorage.tabsRecovery = JSON.stringify( window.localStorage.tabsRecovery = JSON.stringify(
await Promise.all( (await Promise.all(
tabs tabs
.map(tab => { .map(async tab => tab.getRecoveryToken().then(r => {
let token = tab.getRecoveryToken() if (r) {
if (token) { r.tabTitle = tab.title
token = token.then(r => { if (tab.color) {
if (r) { r.tabColor = tab.color
r.tabTitle = tab.title }
if (tab.color) {
r.tabColor = tab.color
}
}
return r
})
} }
return token return r
}) }))
.filter(token => !!token) )).filter(token => !!token)
)
) )
} }
async recoverTab (token: RecoveryToken): Promise<RecoveredTab|null> { async recoverTab (token: RecoveryToken): Promise<RecoveredTab|null> {
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 !== null) { if (tab !== null) {
tab.options = tab.options || {} tab.options = tab.options || {}
tab.options.color = token.tabColor || null tab.options.color = token.tabColor ?? null
tab.options.title = token.tabTitle || '' tab.options.title = token.tabTitle || ''
return tab return tab
} }

View File

@@ -22,7 +22,7 @@ export class TabsService {
const componentRef = componentFactory.create(this.injector) const componentRef = componentFactory.create(this.injector)
const tab = componentRef.instance const tab = componentRef.instance
tab.hostView = componentRef.hostView tab.hostView = componentRef.hostView
Object.assign(tab, inputs || {}) Object.assign(tab, inputs ?? {})
return tab return tab
} }

View File

@@ -18,11 +18,11 @@ export class ThemesService {
} }
findTheme (name: string): Theme|null { findTheme (name: string): Theme|null {
return this.config.enabledServices(this.themes).find(x => x.name === name) || null return this.config.enabledServices(this.themes).find(x => x.name === name) ?? null
} }
findCurrentTheme (): Theme { findCurrentTheme (): Theme {
return this.findTheme(this.config.store.appearance.theme) || this.findTheme('Standard')! return this.findTheme(this.config.store.appearance.theme) ?? this.findTheme('Standard')!
} }
applyTheme (theme: Theme): void { applyTheme (theme: Theme): void {

View File

@@ -33,6 +33,7 @@ export class TouchbarService {
app.tabOpened$.subscribe(tab => { app.tabOpened$.subscribe(tab => {
tab.titleChange$.subscribe(title => { tab.titleChange$.subscribe(title => {
const segment = this.tabSegments[app.tabs.indexOf(tab)] const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) { if (segment) {
segment.label = this.shortenTitle(title) segment.label = this.shortenTitle(title)
this.tabsSegmentedControl.segments = this.tabSegments this.tabsSegmentedControl.segments = this.tabSegments
@@ -41,6 +42,7 @@ export class TouchbarService {
tab.activity$.subscribe(hasActivity => { tab.activity$.subscribe(hasActivity => {
const showIcon = this.app.activeTab !== tab && hasActivity const showIcon = this.app.activeTab !== tab && hasActivity
const segment = this.tabSegments[app.tabs.indexOf(tab)] const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) { if (segment) {
segment.icon = showIcon ? activityIcon : undefined segment.icon = showIcon ? activityIcon : undefined
} }
@@ -53,7 +55,7 @@ export class TouchbarService {
label: this.shortenTitle(tab.title), label: this.shortenTitle(tab.title),
})) }))
this.tabsSegmentedControl.segments = this.tabSegments this.tabsSegmentedControl.segments = this.tabSegments
this.tabsSegmentedControl.selectedIndex = this.app.tabs.indexOf(this.app.activeTab) this.tabsSegmentedControl.selectedIndex = this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : 0
} }
update (): void { update (): void {
@@ -66,14 +68,14 @@ export class TouchbarService {
buttons = buttons.concat(provider.provide()) buttons = buttons.concat(provider.provide())
}) })
buttons = buttons.filter(x => !!x.touchBarNSImage) buttons = buttons.filter(x => !!x.touchBarNSImage)
buttons.sort((a, b) => (a.weight || 0) - (b.weight || 0)) buttons.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
this.tabSegments = this.app.tabs.map(tab => ({ this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title), label: this.shortenTitle(tab.title),
})) }))
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({ this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
segments: this.tabSegments, segments: this.tabSegments,
selectedIndex: this.app.tabs.indexOf(this.app.activeTab), selectedIndex: this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined,
change: (selectedIndex) => this.zone.run(() => { change: (selectedIndex) => this.zone.run(() => {
this.app.selectTab(this.app.tabs[selectedIndex]) this.app.selectTab(this.app.tabs[selectedIndex])
}), }),
@@ -102,13 +104,14 @@ export class TouchbarService {
private getButton (button: ToolbarButton): SegmentedControlSegment { private getButton (button: ToolbarButton): SegmentedControlSegment {
return { return {
label: button.touchBarNSImage ? undefined : this.shortenTitle(button.touchBarTitle || button.title), label: button.touchBarNSImage ? undefined : this.shortenTitle(button.touchBarTitle ?? button.title),
icon: button.touchBarNSImage ? this.getCachedNSImage(button.touchBarNSImage) : undefined, icon: button.touchBarNSImage ? this.getCachedNSImage(button.touchBarNSImage) : undefined,
// click: () => this.zone.run(() => button.click()), // click: () => this.zone.run(() => button.click()),
} }
} }
private getCachedNSImage (name: string) { private getCachedNSImage (name: string) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!this.nsImageCache[name]) { if (!this.nsImageCache[name]) {
this.nsImageCache[name] = this.electron.nativeImage.createFromNamedImage(name, [0, 0, 1]) this.nsImageCache[name] = this.electron.nativeImage.createFromNamedImage(name, [0, 0, 1])
} }

View File

@@ -35,6 +35,10 @@ export class UpdaterService {
this.logger.info('No updates') this.logger.info('No updates')
}) })
electron.autoUpdater.once('error', err => {
this.logger.error(err)
})
this.downloaded = new Promise<boolean>(resolve => { this.downloaded = new Promise<boolean>(resolve => {
electron.autoUpdater.once('update-downloaded', () => resolve(true)) electron.autoUpdater.once('update-downloaded', () => resolve(true))
}) })

View File

@@ -113,7 +113,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
...items, ...items,
{ {
label: 'Rename', label: 'Rename',
click: () => this.zone.run(() => tabHeader?.showRenameTabModal()), click: () => this.zone.run(() => tabHeader.showRenameTabModal()),
}, },
{ {
label: 'Duplicate', label: 'Duplicate',

View File

@@ -40,8 +40,8 @@ module.exports = {
}, },
}, },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'], include: /component\.css/ }, { test: /\.css$/, use: ['@terminus-term/to-string-loader', 'css-loader'], include: /component\.css/ },
{ test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /component\.css/ }, { test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /component\.css/ },
{ test: /\.yaml$/, use: ['json-loader', 'yaml-loader'] }, { test: /\.yaml$/, use: ['json-loader', 'yaml-loader'] },
{ test: /\.svg/, use: ['svg-inline-loader'] }, { test: /\.svg/, use: ['svg-inline-loader'] },

View File

@@ -124,9 +124,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.7.0" version "3.8.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.7.0.tgz#b0a761a02488577afbf97179e4681bf49568520f" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.2.tgz#0a1fd6709246da9ca8eff5bb0cbd15fba9ac7044"
integrity sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA== integrity sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==
core-util-is@~1.0.0: core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"

View File

@@ -19,6 +19,7 @@
"devDependencies": { "devDependencies": {
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"axios": "^0.19.0", "axios": "^0.19.0",
"electron-promise-ipc": "^2.2.4",
"mz": "^2.6.0", "mz": "^2.6.0",
"semver": "^7.1.1" "semver": "^7.1.1"
}, },

View File

@@ -22,10 +22,10 @@
button.btn.btn-primary.ml-2( button.btn.btn-primary.ml-2(
*ngIf='knownUpgrades[plugin.name]', *ngIf='knownUpgrades[plugin.name]',
(click)='upgradePlugin(plugin)', (click)='upgradePlugin(plugin)',
[disabled]='busy[plugin.name] != undefined' [disabled]='busy.has(plugin.name)'
) )
i.fas.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing') i.fas.fa-fw.fa-arrow-up(*ngIf='busy.get(plugin.name) != BusyState.Installing')
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.get(plugin.name) == BusyState.Installing')
span Upgrade ({{knownUpgrades[plugin.name].version}}) span Upgrade ({{knownUpgrades[plugin.name].version}})
button.btn.btn-link.text-primary.ml-2( button.btn.btn-link.text-primary.ml-2(
@@ -43,10 +43,10 @@
button.btn.btn-link.text-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.has(plugin.name)'
) )
i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling') i.fas.fa-fw.fa-trash(*ngIf='busy.get(plugin.name) != BusyState.Uninstalling')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling') i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy.get(plugin.name) == BusyState.Uninstalling')
div div
h3.mt-4 Available h3.mt-4 Available
@@ -69,10 +69,10 @@ div
.list-group-item.d-flex.align-items-center(*ngIf='!isAlreadyInstalled(plugin)') .list-group-item.d-flex.align-items-center(*ngIf='!isAlreadyInstalled(plugin)')
button.btn.btn-primary.mr-3( button.btn.btn-primary.mr-3(
(click)='installPlugin(plugin)', (click)='installPlugin(plugin)',
[disabled]='busy[plugin.name] != undefined' [disabled]='busy.has(plugin.name)'
) )
i.fas.fa-fw.fa-download(*ngIf='busy[plugin.name] != BusyState.Installing') i.fas.fa-fw.fa-download(*ngIf='busy.get(plugin.name) != BusyState.Installing')
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.get(plugin.name) == BusyState.Installing')
div((click)='showPluginInfo(plugin)') div((click)='showPluginInfo(plugin)')
div div

View File

@@ -20,7 +20,7 @@ export class PluginsSettingsTabComponent {
@Input() availablePluginsQuery$ = new BehaviorSubject<string>('') @Input() availablePluginsQuery$ = new BehaviorSubject<string>('')
@Input() availablePluginsReady = false @Input() availablePluginsReady = false
@Input() knownUpgrades: Record<string, PluginInfo|null> = {} @Input() knownUpgrades: Record<string, PluginInfo|null> = {}
@Input() busy: Record<string, BusyState> = {} @Input() busy = new Map<string, BusyState>()
@Input() erroredPlugin: string @Input() erroredPlugin: string
@Input() errorMessage: string @Input() errorMessage: string
@@ -67,29 +67,29 @@ export class PluginsSettingsTabComponent {
} }
async installPlugin (plugin: PluginInfo): Promise<void> { async installPlugin (plugin: PluginInfo): Promise<void> {
this.busy[plugin.name] = BusyState.Installing this.busy.set(plugin.name, BusyState.Installing)
try { try {
await this.pluginManager.installPlugin(plugin) await this.pluginManager.installPlugin(plugin)
delete this.busy[plugin.name] this.busy.delete(plugin.name)
this.config.requestRestart() this.config.requestRestart()
} catch (err) { } catch (err) {
this.erroredPlugin = plugin.name this.erroredPlugin = plugin.name
this.errorMessage = err this.errorMessage = err
delete this.busy[plugin.name] this.busy.delete(plugin.name)
throw err throw err
} }
} }
async uninstallPlugin (plugin: PluginInfo): Promise<void> { async uninstallPlugin (plugin: PluginInfo): Promise<void> {
this.busy[plugin.name] = BusyState.Uninstalling this.busy.set(plugin.name, BusyState.Uninstalling)
try { try {
await this.pluginManager.uninstallPlugin(plugin) await this.pluginManager.uninstallPlugin(plugin)
delete this.busy[plugin.name] this.busy.delete(plugin.name)
this.config.requestRestart() this.config.requestRestart()
} catch (err) { } catch (err) {
this.erroredPlugin = plugin.name this.erroredPlugin = plugin.name
this.errorMessage = err this.errorMessage = err
delete this.busy[plugin.name] this.busy.delete(plugin.name)
throw err throw err
} }
} }

View File

@@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import promiseIpc from 'electron-promise-ipc'
import { Observable, from } from 'rxjs' import { Observable, from } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
@@ -31,39 +32,15 @@ export class PluginManagerService {
userPluginsPath: string = (window as any).userPluginsPath userPluginsPath: string = (window as any).userPluginsPath
installedPlugins: PluginInfo[] = (window as any).installedPlugins installedPlugins: PluginInfo[] = (window as any).installedPlugins
private npmReady: Promise<void>
private npm: any
private constructor ( private constructor (
log: LogService, log: LogService,
) { ) {
this.logger = log.create('pluginManager') this.logger = log.create('pluginManager')
} }
async getNPM (): Promise<any> {
if (!this.npm) {
if (!this.npmReady) {
this.npmReady = new Promise(resolve => {
const npm = (global as any).require('npm')
npm.load({
prefix: this.userPluginsPath,
}, err => {
if (err) {
this.logger.error(err)
}
this.npm = npm
resolve()
})
})
}
await this.npmReady
}
return this.npm
}
listAvailable (query?: string): Observable<PluginInfo[]> { listAvailable (query?: string): Observable<PluginInfo[]> {
return from( return from(
axios.get(`https://www.npmjs.com/search?q=keywords%3A${KEYWORD}+${encodeURIComponent(query || '')}&from=0&size=1000`, { axios.get(`https://www.npmjs.com/search?q=keywords%3A${KEYWORD}+${encodeURIComponent(query ?? '')}&from=0&size=1000`, {
headers: { headers: {
'x-spiferack': '1', 'x-spiferack': '1',
}, },
@@ -84,21 +61,23 @@ export class PluginManagerService {
} }
async installPlugin (plugin: PluginInfo): Promise<void> { async installPlugin (plugin: PluginInfo): Promise<void> {
(await this.getNPM()).commands.install([`${plugin.packageName}@${plugin.version}`], err => { try {
if (err) { await (promiseIpc as any).send('plugin-manager:install', this.userPluginsPath, plugin.packageName, plugin.version)
this.logger.error(err)
}
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName) this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
this.installedPlugins.push(plugin) this.installedPlugins.push(plugin)
}) } catch (err) {
this.logger.error(err)
throw err
}
} }
async uninstallPlugin (plugin: PluginInfo): Promise<void> { async uninstallPlugin (plugin: PluginInfo): Promise<void> {
(await this.getNPM()).commands.remove([plugin.packageName], err => { try {
if (err) { await (promiseIpc as any).send('plugin-manager:uninstall', this.userPluginsPath, plugin.packageName)
this.logger.error(err)
}
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName) this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
}) } catch (err) {
this.logger.error(err)
throw err
}
} }
} }

View File

@@ -40,14 +40,14 @@ module.exports = {
}, },
}, },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
], ],
}, },
externals: [ externals: [
'fs', 'fs',
'net', 'net',
'npm',
'path', 'path',
'electron-promise-ipc',
/^rxjs/, /^rxjs/,
/^@angular/, /^@angular/,
/^@ng-bootstrap/, /^@ng-bootstrap/,

View File

@@ -19,6 +19,14 @@ axios@^0.19.0:
dependencies: dependencies:
follow-redirects "1.5.10" follow-redirects "1.5.10"
call-bind@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.0"
debug@=3.1.0: debug@=3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@@ -26,6 +34,50 @@ debug@=3.1.0:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
dependencies:
object-keys "^1.0.12"
electron-promise-ipc@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/electron-promise-ipc/-/electron-promise-ipc-2.2.4.tgz#b82daf86ca6d0f0b8655937fdbe9a554590deeea"
integrity sha512-xCkFEeuru9l7H/+m1gpK4F1utexvTT7+n1PTquP2MVTpmBmpgFBlLqSXC7TqwpROkHRm9wGpaCJEx0djonnSEg==
dependencies:
is-electron-renderer "^2.0.1"
object.entries "^1.1.3"
serialize-error "^5.0.0"
uuid "^3.0.1"
es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.2"
is-negative-zero "^2.0.0"
is-regex "^1.1.1"
object-inspect "^1.8.0"
object-keys "^1.1.1"
object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
follow-redirects@1.5.10: follow-redirects@1.5.10:
version "1.5.10" version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
@@ -33,6 +85,66 @@ follow-redirects@1.5.10:
dependencies: dependencies:
debug "=3.1.0" debug "=3.1.0"
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
get-intrinsic@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
is-date-object@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
is-electron-renderer@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz#a469d056f975697c58c98c6023eb0aa79af895a2"
integrity sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=
is-negative-zero@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
dependencies:
has-symbols "^1.0.1"
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
dependencies:
has-symbols "^1.0.1"
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"
@@ -52,11 +164,64 @@ 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=
object-inspect@^1.8.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object.assign@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
has-symbols "^1.0.1"
object-keys "^1.1.1"
object.entries@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
has "^1.0.3"
semver@^7.1.1: semver@^7.1.1:
version "7.3.2" version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
serialize-error@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac"
integrity sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==
dependencies:
type-fest "^0.8.0"
string.prototype.trimend@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
string.prototype.trimstart@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
thenify-all@^1.0.0: thenify-all@^1.0.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
@@ -70,3 +235,13 @@ thenify-all@^1.0.0:
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies: dependencies:
any-promise "^1.0.0" any-promise "^1.0.0"
type-fest@^0.8.0:
version "0.8.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
uuid@^3.0.1:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

View File

@@ -20,6 +20,7 @@
"@types/node": "14.14.14", "@types/node": "14.14.14",
"@types/ssh2": "^0.5.35", "@types/ssh2": "^0.5.35",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"buffer-replace": "^1.0.0",
"cli-spinner": "^0.2.10" "cli-spinner": "^0.2.10"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -1,7 +1,12 @@
import stripAnsi from 'strip-ansi'
import bufferReplace from 'buffer-replace'
import { BaseSession } from 'terminus-terminal' import { BaseSession } from 'terminus-terminal'
import { SerialPort } from 'serialport' import { SerialPort } from 'serialport'
import { Logger } from 'terminus-core' import { Logger } from 'terminus-core'
import { Subject, Observable } from 'rxjs' import { Subject, Observable, interval } from 'rxjs'
import { debounce } from 'rxjs/operators'
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
import { PassThrough, Readable, Writable } from 'stream'
export interface LoginScript { export interface LoginScript {
expect: string expect: string
@@ -23,10 +28,13 @@ export interface SerialConnection {
xany: boolean xany: boolean
scripts?: LoginScript[] scripts?: LoginScript[]
color?: string color?: string
inputMode?: InputMode
inputNewlines?: NewlineMode
outputNewlines?: NewlineMode
} }
export const BAUD_RATES = [ export const BAUD_RATES = [
110, 150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 110, 150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000,
] ]
export interface SerialPortInfo { export interface SerialPortInfo {
@@ -34,6 +42,9 @@ export interface SerialPortInfo {
description?: string description?: string
} }
export type InputMode = null | 'readline' // eslint-disable-line @typescript-eslint/no-type-alias
export type NewlineMode = null | 'cr' | 'lf' | 'crlf' // eslint-disable-line @typescript-eslint/no-type-alias
export class SerialSession extends BaseSession { export class SerialSession extends BaseSession {
scripts?: LoginScript[] scripts?: LoginScript[]
serial: SerialPort serial: SerialPort
@@ -41,58 +52,38 @@ export class SerialSession extends BaseSession {
get serviceMessage$ (): Observable<string> { return this.serviceMessage } get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>() private serviceMessage = new Subject<string>()
private inputReadline: ReadLine
private inputPromptVisible = true
private inputReadlineInStream: Readable & Writable
private inputReadlineOutStream: Readable & Writable
constructor (public connection: SerialConnection) { constructor (public connection: SerialConnection) {
super() super()
this.scripts = connection.scripts || [] this.scripts = connection.scripts ?? []
this.inputReadlineInStream = new PassThrough()
this.inputReadlineOutStream = new PassThrough()
this.inputReadline = createReadline({
input: this.inputReadlineInStream,
output: this.inputReadlineOutStream,
terminal: true,
} as any)
this.inputReadlineOutStream.on('data', data => {
if (this.connection.inputMode === 'readline') {
this.emitOutput(data)
}
})
this.inputReadline.on('line', line => {
this.onInput(new Buffer(line + '\n'))
})
this.output$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled())
} }
async start (): Promise<void> { async start (): Promise<void> {
this.open = true this.open = true
this.serial.on('data', data => { this.serial.on('readable', () => {
const dataString = data.toString() this.onOutput(this.serial.read())
this.emitOutput(data)
if (this.scripts) {
let found = false
for (const script of this.scripts) {
let match = false
let cmd = ''
if (script.isRegex) {
const re = new RegExp(script.expect, 'g')
if (dataString.match(re)) {
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else {
if (dataString.includes(script.expect)) {
cmd = script.send
match = true
found = true
}
}
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.serial.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
this.logger.debug('Skip optional script: ' + script.expect)
found = true
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
if (found) {
this.executeUnconditionalScripts()
}
}
}) })
this.serial.on('end', () => { this.serial.on('end', () => {
@@ -106,23 +97,33 @@ export class SerialSession extends BaseSession {
} }
write (data: Buffer): void { write (data: Buffer): void {
if (this.serial) { if (this.connection.inputMode === 'readline') {
this.serial.write(data.toString()) this.inputReadlineInStream.write(data)
} else {
this.onInput(data)
} }
} }
async destroy (): Promise<void> { async destroy (): Promise<void> {
this.serviceMessage.complete() this.serviceMessage.complete()
this.inputReadline.close()
await super.destroy() await super.destroy()
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
resize (_, __) { } resize (_, __) {
this.inputReadlineOutStream.emit('resize')
}
kill (_?: string): void { kill (_?: string): void {
this.serial.close() this.serial.close()
} }
emitServiceMessage (msg: string): void {
this.serviceMessage.next(msg)
this.logger.info(stripAnsi(msg))
}
async getChildProcesses (): Promise<any[]> { async getChildProcesses (): Promise<any[]> {
return [] return []
} }
@@ -131,10 +132,102 @@ export class SerialSession extends BaseSession {
this.kill('TERM') this.kill('TERM')
} }
supportsWorkingDirectory (): boolean {
return false
}
async getWorkingDirectory (): Promise<string|null> { async getWorkingDirectory (): Promise<string|null> {
return null return null
} }
private replaceNewlines (data: Buffer, mode?: NewlineMode): Buffer {
if (!mode) {
return data
}
data = bufferReplace(data, '\r\n', '\n')
data = bufferReplace(data, '\r', '\n')
const replacement = {
strip: '',
cr: '\r',
lf: '\n',
crlf: '\r\n',
}[mode]
return bufferReplace(data, '\n', replacement)
}
private onInput (data: Buffer) {
data = this.replaceNewlines(data, this.connection.inputNewlines)
if (this.serial) {
this.serial.write(data.toString())
}
}
private onOutputSettled () {
if (this.connection.inputMode === 'readline' && !this.inputPromptVisible) {
this.resetInputPrompt()
}
}
private resetInputPrompt () {
this.emitOutput(new Buffer('\r\n'))
this.inputReadline.prompt(true)
this.inputPromptVisible = true
}
private onOutput (data: Buffer) {
const dataString = data.toString()
if (this.connection.inputMode === 'readline') {
if (this.inputPromptVisible) {
clearLine(this.inputReadlineOutStream, 0)
this.inputPromptVisible = false
}
}
data = this.replaceNewlines(data, this.connection.outputNewlines)
this.emitOutput(data)
if (this.scripts) {
let found = false
for (const script of this.scripts) {
let match = false
let cmd = ''
if (script.isRegex) {
const re = new RegExp(script.expect, 'g')
if (re.test(dataString)) {
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else {
if (dataString.includes(script.expect)) {
cmd = script.send
match = true
found = true
}
}
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.serial.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
this.logger.debug('Skip optional script: ' + script.expect)
found = true
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
if (found) {
this.executeUnconditionalScripts()
}
}
}
private executeUnconditionalScripts () { private executeUnconditionalScripts () {
if (this.scripts) { if (this.scripts) {
for (const script of this.scripts) { for (const script of this.scripts) {

View File

@@ -28,7 +28,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
weight: 5, weight: 5,
title: 'Serial connections', title: 'Serial connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate', touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: async () => { click: () => {
this.activate() this.activate()
}, },
}] }]

View File

@@ -11,23 +11,86 @@
[(ngModel)]='connection.name', [(ngModel)]='connection.name',
) )
.form-group .row
label Path .col-6
input.form-control( .form-group
type='text', label Path
[(ngModel)]='connection.port', input.form-control(
[ngbTypeahead]='portsAutocomplete', type='text',
[resultFormatter]='portsFormatter', [(ngModel)]='connection.port',
) [ngbTypeahead]='portsAutocomplete',
[resultFormatter]='portsFormatter',
)
.form-group .col-6
label Baud Rate .form-group
select.form-control( label Baud Rate
[(ngModel)]='connection.baudrate', select.form-control(
) [(ngModel)]='connection.baudrate',
option([value]='x', *ngFor='let x of baudRates') {{x}} )
option([value]='x', *ngFor='let x of baudRates') {{x}}
ngb-tab(id='advanced') .row
.col-6
.form-line
.header
.title Input mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getInputModeName(connection.inputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of inputModes',
(click)='connection.inputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.col-6
.form-line
.header
.title Input newlines
select.form-control(
[(ngModel)]='connection.inputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
.row
.col-6
//- .form-line
.header
.title Output mode
.d-flex(ngbDropdown)
button.btn.btn-secondary.btn-tab-bar(
ngbDropdownToggle,
) {{getInputModeName(connection.inputMode)}}
div(ngbDropdownMenu)
a.d-flex.flex-column(
*ngFor='let mode of inputModes',
(click)='connection.inputMode = mode.key',
ngbDropdownItem
)
div {{mode.name}}
.text-muted {{mode.description}}
.col-6
.form-line
.header
.title Output newlines
select.form-control(
[(ngModel)]='connection.outputNewlines',
)
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
ngb-tab(id='advanced')
ng-template(ngbTabTitle) Advanced ng-template(ngbTabTitle) Advanced
ng-template(ngbTabContent) ng-template(ngbTabContent)
.form-line .form-line

View File

@@ -15,6 +15,17 @@ export class EditConnectionModalComponent {
connection: SerialConnection connection: SerialConnection
foundPorts: SerialPortInfo[] foundPorts: SerialPortInfo[]
baudRates = BAUD_RATES baudRates = BAUD_RATES
inputModes = [
{ key: null, name: 'Normal', description: 'Input is sent as you type' },
{ key: 'readline', name: 'Line by line', description: 'Line editor, input is sent after you press Enter' },
]
newlineModes = [
{ key: null, name: 'Keep' },
{ key: 'strip', name: 'Strip' },
{ key: 'cr', name: 'Force CR' },
{ key: 'lf', name: 'Force LF' },
{ key: 'crlf', name: 'Force CRLF' },
]
constructor ( constructor (
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
@@ -24,6 +35,10 @@ export class EditConnectionModalComponent {
) { ) {
} }
getInputModeName (key) {
return this.inputModes.find(x => x.key === key)?.name
}
portsAutocomplete = text$ => text$.pipe(map(() => { portsAutocomplete = text$ => text$.pipe(map(() => {
return this.foundPorts.map(x => x.name) return this.foundPorts.map(x => x.name)
})) }))
@@ -37,7 +52,7 @@ export class EditConnectionModalComponent {
} }
async ngOnInit () { async ngOnInit () {
this.connection.scripts = this.connection.scripts || [] this.connection.scripts = this.connection.scripts ?? []
this.foundPorts = await this.serial.listPorts() this.foundPorts = await this.serial.listPorts()
} }

View File

@@ -34,6 +34,9 @@ export class SerialSettingsTabComponent {
xany: false, xany: false,
xoff: false, xoff: false,
xon: false, xon: false,
inputMode: null,
inputNewlines: null,
outputNewlines: null,
} }
const modal = this.ngbModal.open(EditConnectionModalComponent) const modal = this.ngbModal.open(EditConnectionModalComponent)

View File

@@ -1,16 +1,16 @@
.tab-toolbar .tab-toolbar
.btn.btn-outline-secondary.reveal-button .btn.btn-outline-secondary.reveal-button
i.fas.fa-ellipsis-h i.fas.fa-ellipsis-h
.toolbar(*ngIf='session', [class.show]='!session.open') .toolbar([class.show]='!session || !session.open')
i.fas.fa-circle.text-success.mr-2(*ngIf='session.open') i.fas.fa-circle.text-success.mr-2(*ngIf='session && session.open')
i.fas.fa-circle.text-danger.mr-2(*ngIf='!session.open') i.fas.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open')
strong(*ngIf='session') {{session.connection.port}} ({{session.connection.baudrate}}) strong(*ngIf='session') {{session.connection.port}} ({{session.connection.baudrate}})
.mr-auto .mr-auto
button.btn.btn-secondary.mr-3((click)='changeBaudRate()', *ngIf='session.open') button.btn.btn-secondary.mr-3((click)='changeBaudRate()', *ngIf='session && session.open')
span Change baud rate span Change baud rate
button.btn.btn-info((click)='reconnect()', *ngIf='!session.open') button.btn.btn-info((click)='reconnect()', *ngIf='!session || !session.open')
i.fas.fa-reload i.fas.fa-reload
span Reconnect span Reconnect

View File

@@ -11,14 +11,15 @@ import { Subscription } from 'rxjs'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'serial-tab', selector: 'serial-tab',
template: BaseTerminalTabComponent.template + (require('./serialTab.component.pug') as string), template: `${BaseTerminalTabComponent.template} ${require('./serialTab.component.pug')}`,
styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles], styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class SerialTabComponent extends BaseTerminalTabComponent { export class SerialTabComponent extends BaseTerminalTabComponent {
connection: SerialConnection connection?: SerialConnection
session: SerialSession session: SerialSession|null = null
serialPort: any serialPort: any
private serialService: SerialService
private homeEndSubscription: Subscription private homeEndSubscription: Subscription
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor
@@ -26,6 +27,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
injector: Injector, injector: Injector,
) { ) {
super(injector) super(injector)
this.serialService = injector.get(SerialService)
} }
ngOnInit () { ngOnInit () {
@@ -52,7 +54,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
super.ngOnInit() super.ngOnInit()
setImmediate(() => { setImmediate(() => {
this.setTitle(this.connection.name) this.setTitle(this.connection!.name)
}) })
} }
@@ -62,12 +64,8 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
return return
} }
this.session = this.injector.get(SerialService).createSession(this.connection) const session = this.serialService.createSession(this.connection)
this.session.serviceMessage$.subscribe(msg => { this.setSession(session)
this.write(`\r\n${colors.black.bgWhite(' serial ')} ${msg}\r\n`)
this.session.resize(this.size.columns, this.size.rows)
})
this.attachSessionHandlers()
this.write(`Connecting to `) this.write(`Connecting to `)
const spinner = new Spinner({ const spinner = new Spinner({
@@ -80,15 +78,32 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
spinner.start() spinner.start()
try { try {
this.serialPort = await this.injector.get(SerialService).connectSession(this.session) this.serialPort = await this.serialService.connectSession(this.session!)
spinner.stop(true) spinner.stop(true)
session.emitServiceMessage('Port opened')
} catch (e) { } catch (e) {
spinner.stop(true) spinner.stop(true)
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n') this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
return return
} }
await this.session.start() await this.session!.start()
this.session.resize(this.size.columns, this.size.rows) this.session!.resize(this.size.columns, this.size.rows)
}
protected attachSessionHandlers () {
this.attachSessionHandler(this.session!.serviceMessage$.subscribe(msg => {
this.write(`\r\n${colors.black.bgWhite(' Serial ')} ${msg}\r\n`)
this.session?.resize(this.size.columns, this.size.rows)
}))
this.attachSessionHandler(this.session!.destroyed$.subscribe(() => {
this.write('Press any key to reconnect\r\n')
this.input$.pipe(first()).subscribe(() => {
if (!this.session?.open) {
this.reconnect()
}
})
}))
super.attachSessionHandlers()
} }
async getRecoveryToken (): Promise<any> { async getRecoveryToken (): Promise<any> {
@@ -99,8 +114,10 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
} }
} }
reconnect () { async reconnect (): Promise<void> {
this.initializeSession() this.session?.destroy()
await this.initializeSession()
this.session?.releaseInitialDataBuffer()
} }
async changeBaudRate () { async changeBaudRate () {
@@ -108,7 +125,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
name: x.toString(), result: x, name: x.toString(), result: x,
}))) })))
this.serialPort.update({ baudRate: rate }) this.serialPort.update({ baudRate: rate })
this.connection.baudrate = rate this.connection!.baudrate = rate
} }
ngOnDestroy () { ngOnDestroy () {

View File

@@ -7,7 +7,7 @@ import { SerialTabComponent } from './components/serialTab.component'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> { async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
if (recoveryToken?.type === 'app:serial-tab') { if (recoveryToken.type === 'app:serial-tab') {
return { return {
type: SerialTabComponent, type: SerialTabComponent,
options: { options: {

View File

@@ -30,10 +30,17 @@ export class SerialService {
} }
async connectSession (session: SerialSession): Promise<SerialPort> { async connectSession (session: SerialSession): Promise<SerialPort> {
const serial = new SerialPort(session.connection.port, { autoOpen: false, baudRate: session.connection.baudrate, const serial = new SerialPort(session.connection.port, {
dataBits: session.connection.databits, stopBits: session.connection.stopbits, parity: session.connection.parity, autoOpen: false,
rtscts: session.connection.rtscts, xon: session.connection.xon, xoff: session.connection.xoff, baudRate: session.connection.baudrate,
xany: session.connection.xany }) dataBits: session.connection.databits,
stopBits: session.connection.stopbits,
parity: session.connection.parity,
rtscts: session.connection.rtscts,
xon: session.connection.xon,
xoff: session.connection.xoff,
xany: session.connection.xany,
})
session.serial = serial session.serial = serial
let connected = false let connected = false
await new Promise(async (resolve, reject) => { await new Promise(async (resolve, reject) => {
@@ -50,6 +57,10 @@ export class SerialService {
} }
}) })
}) })
serial.on('close', () => {
session.emitServiceMessage('Port closed')
session.destroy()
})
try { try {
serial.open() serial.open()
@@ -57,7 +68,6 @@ export class SerialService {
this.toastr.error(e.message) this.toastr.error(e.message)
reject(e) reject(e)
} }
}) })
return serial return serial
} }
@@ -104,7 +114,7 @@ export class SerialService {
options.push({ options.push({
name: 'Manage connections', name: 'Manage connections',
icon: 'cog', icon: 'cog',
callback: () => this.app.openNewTab(SettingsTabComponent, { activeTab: 'serial' }), callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'serial' }),
}) })
options.push({ options.push({
@@ -125,10 +135,10 @@ export class SerialService {
{ connection } { connection }
) as SerialTabComponent ) as SerialTabComponent
if (connection.color) { if (connection.color) {
(this.app.getParentTab(tab) || tab).color = connection.color (this.app.getParentTab(tab) ?? tab).color = connection.color
} }
setTimeout(() => { setTimeout(() => {
this.app.activeTab.emitFocused() this.app.activeTab?.emitFocused()
}) })
return tab return tab
} catch (error) { } catch (error) {

View File

@@ -40,7 +40,7 @@ module.exports = {
}, },
}, },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.svg/, use: ['svg-inline-loader'] }, { test: /\.svg/, use: ['svg-inline-loader'] },
], ],
}, },
@@ -50,6 +50,8 @@ module.exports = {
'path', 'path',
'ngx-toastr', 'ngx-toastr',
'serialport', 'serialport',
'readline',
'stream',
'windows-process-tree/build/Release/windows_process_tree.node', 'windows-process-tree/build/Release/windows_process_tree.node',
/^rxjs/, /^rxjs/,
/^@angular/, /^@angular/,

View File

@@ -32,6 +32,11 @@ ansi-colors@^4.1.1:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
buffer-replace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-replace/-/buffer-replace-1.0.0.tgz#bc427c40af4c1f06d6933dede57110acba8ade54"
integrity sha1-vEJ8QK9MHwbWkz3t5XEQrLqK3lQ=
cli-spinner@^0.2.10: cli-spinner@^0.2.10:
version "0.2.10" version "0.2.10"
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47" resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"

View File

@@ -10,7 +10,7 @@ import { HotkeyInputModalComponent } from './hotkeyInputModal.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MultiHotkeyInputComponent { export class MultiHotkeyInputComponent {
@Input() model: string[][] @Input() model: string[][] = []
@Output() modelChange = new EventEmitter() @Output() modelChange = new EventEmitter()
constructor ( constructor (
@@ -18,9 +18,6 @@ export class MultiHotkeyInputComponent {
) { } ) { }
ngOnInit (): void { ngOnInit (): void {
if (!this.model) {
this.model = []
}
if (typeof this.model === 'string') { if (typeof this.model === 'string') {
this.model = [this.model] this.model = [this.model]
} }

View File

@@ -307,7 +307,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
td {{hotkey.id}} td {{hotkey.id}}
td.pr-5 td.pr-5
multi-hotkey-input( multi-hotkey-input(
[model]='getHotkey(hotkey.id)', [model]='getHotkey(hotkey.id) || []',
(modelChange)='setHotkey(hotkey.id, $event); config.save(); docking.dock()' (modelChange)='setHotkey(hotkey.id, $event); config.save(); docking.dock()'
) )

View File

@@ -60,7 +60,7 @@ export class SettingsTabComponent extends BaseTabComponent {
this.settingsProviders = config.enabledServices(this.settingsProviders) this.settingsProviders = config.enabledServices(this.settingsProviders)
this.themes = config.enabledServices(this.themes) this.themes = config.enabledServices(this.themes)
this.configDefaults = yaml.safeDump(config.getDefaults()) this.configDefaults = yaml.dump(config.getDefaults())
const onConfigChange = () => { const onConfigChange = () => {
this.configFile = config.readRaw() this.configFile = config.readRaw()
@@ -116,7 +116,7 @@ export class SettingsTabComponent extends BaseTabComponent {
isConfigFileValid () { isConfigFileValid () {
try { try {
yaml.safeLoad(this.configFile) yaml.load(this.configFile)
return true return true
} catch (_) { } catch (_) {
return false return false

View File

@@ -40,8 +40,8 @@ module.exports = {
}, },
}, },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.svg/, use: ['svg-inline-loader'] }, { test: /\.svg/, use: ['svg-inline-loader'] },
], ],
}, },

View File

@@ -24,7 +24,7 @@ export enum SSHAlgorithmType {
export interface SSHConnection { export interface SSHConnection {
name: string name: string
host: string host: string
port: number port?: number
user: string user: string
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive' auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
password?: string password?: string
@@ -76,10 +76,10 @@ export class ForwardedPort {
}) })
} else if (this.type === PortForwardType.Dynamic) { } else if (this.type === PortForwardType.Dynamic) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.listener = socksv5.createServer((info, accept, reject) => { this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
callback( callback(
() => accept(true), () => acceptConnection(true),
() => reject(), () => rejectConnection(),
null, null,
null, null,
info.dstAddr, info.dstAddr,
@@ -112,7 +112,7 @@ export class ForwardedPort {
export class SSHSession extends BaseSession { export class SSHSession extends BaseSession {
scripts?: LoginScript[] scripts?: LoginScript[]
shell: ClientChannel shell?: ClientChannel
ssh: Client ssh: Client
forwardedPorts: ForwardedPort[] = [] forwardedPorts: ForwardedPort[] = []
logger: Logger logger: Logger
@@ -123,7 +123,7 @@ export class SSHSession extends BaseSession {
constructor (public connection: SSHConnection) { constructor (public connection: SSHConnection) {
super() super()
this.scripts = connection.scripts || [] this.scripts = connection.scripts ?? []
this.destroyed$.subscribe(() => { this.destroyed$.subscribe(() => {
for (const port of this.forwardedPorts) { for (const port of this.forwardedPorts) {
if (port.type === PortForwardType.Local) { if (port.type === PortForwardType.Local) {
@@ -140,6 +140,9 @@ export class SSHSession extends BaseSession {
this.shell = await this.openShellChannel({ x11: this.connection.x11 }) this.shell = await this.openShellChannel({ x11: this.connection.x11 })
} catch (err) { } catch (err) {
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`)
if (err.toString().includes('Unable to request X11')) {
this.emitServiceMessage(' Make sure `xauth` is installed on the remote side')
}
return return
} }
@@ -177,7 +180,7 @@ export class SSHSession extends BaseSession {
if (match) { if (match) {
this.logger.info('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) {
@@ -213,6 +216,7 @@ export class SSHSession extends BaseSession {
const socket = new Socket() const socket = new Socket()
socket.connect(forward.targetPort, forward.targetAddress) socket.connect(forward.targetPort, forward.targetAddress)
socket.on('error', e => { socket.on('error', e => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
reject() reject()
}) })
@@ -232,7 +236,7 @@ export class SSHSession extends BaseSession {
this.ssh.on('x11', (details, accept, reject) => { this.ssh.on('x11', (details, accept, reject) => {
this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`) this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
const displaySpec = process.env.DISPLAY || ':0' const displaySpec = process.env.DISPLAY ?? ':0'
this.logger.debug(`Trying display ${displaySpec}`) this.logger.debug(`Trying display ${displaySpec}`)
const xHost = displaySpec.split(':')[0] const xHost = displaySpec.split(':')[0]
const xDisplay = parseInt(displaySpec.split(':')[1].split('.')[0] || '0') const xDisplay = parseInt(displaySpec.split(':')[1].split('.')[0] || '0')
@@ -243,7 +247,14 @@ export class SSHSession extends BaseSession {
socket.connect(xPort, xHost) socket.connect(xPort, xHost)
} }
socket.on('error', e => { socket.on('error', e => {
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server ${xHost}:${xPort}: ${e}`) // eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
this.emitServiceMessage(` Terminus tried to connect to ${xHost}:${xPort} based on the DISPLAY environment var (${displaySpec})`)
if (process.platform === 'win32') {
this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
}
reject() reject()
}) })
socket.on('connect', () => { socket.on('connect', () => {
@@ -273,27 +284,25 @@ export class SSHSession extends BaseSession {
await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => { await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
this.logger.info(`New connection on ${fw}`) this.logger.info(`New connection on ${fw}`)
this.ssh.forwardOut( this.ssh.forwardOut(
sourceAddress || '127.0.0.1', sourceAddress ?? '127.0.0.1',
sourcePort || 0, sourcePort ?? 0,
targetAddress, targetAddress,
targetPort, targetPort,
(err, stream) => { (err, stream) => {
if (err) { if (err) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
reject() return reject()
return
}
if (stream) {
const socket = accept()
stream.pipe(socket)
socket.pipe(stream)
stream.on('close', () => {
socket.destroy()
})
socket.on('close', () => {
stream.close()
})
} }
const socket = accept()
stream.pipe(socket)
socket.pipe(stream)
stream.on('close', () => {
socket.destroy()
})
socket.on('close', () => {
stream.close()
})
} }
) )
}).then(() => { }).then(() => {
@@ -308,6 +317,7 @@ export class SSHSession extends BaseSession {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
this.ssh.forwardIn(fw.host, fw.port, err => { this.ssh.forwardIn(fw.host, fw.port, err => {
if (err) { if (err) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
return reject(err) return reject(err)
} }
@@ -345,7 +355,7 @@ export class SSHSession extends BaseSession {
kill (signal?: string): void { kill (signal?: string): void {
if (this.shell) { if (this.shell) {
this.shell.signal(signal || 'TERM') this.shell.signal(signal ?? 'TERM')
} }
} }
@@ -362,6 +372,10 @@ export class SSHSession extends BaseSession {
this.kill('TERM') this.kill('TERM')
} }
supportsWorkingDirectory (): boolean {
return true
}
async getWorkingDirectory (): Promise<string|null> { async getWorkingDirectory (): Promise<string|null> {
return null return null
} }
@@ -383,7 +397,7 @@ export class SSHSession extends BaseSession {
for (const script of this.scripts) { for (const script of this.scripts) {
if (!script.expect) { if (!script.expect) {
console.log('Executing script:', script.send) console.log('Executing script:', script.send)
this.shell.write(script.send + '\n') this.shell!.write(script.send + '\n')
this.scripts = this.scripts.filter(x => x !== script) this.scripts = this.scripts.filter(x => x !== script)
} else { } else {
break break
@@ -397,3 +411,9 @@ export interface SSHConnectionGroup {
name: string name: string
connections: SSHConnection[] connections: SSHConnection[]
} }
export const ALGORITHM_BLACKLIST = [
// cause native crashes in node crypto, use EC instead
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group-exchange-sha1',
]

View File

@@ -28,7 +28,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
weight: 5, weight: 5,
title: 'SSH connections', title: 'SSH connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate', touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: async () => { click: () => {
this.activate() this.activate()
}, },
}] }]

View File

@@ -103,7 +103,7 @@
.header .header
.title Jump host .title Jump host
select.form-control([(ngModel)]='connection.jumpHost') select.form-control([(ngModel)]='connection.jumpHost')
option([ngValue]='null') None option(value='') None
option([ngValue]='x.name', *ngFor='let x of config.store.ssh.connections') {{x.name}} option([ngValue]='x.name', *ngFor='let x of config.store.ssh.connections') {{x.name}}
.form-line .form-line

View File

@@ -3,7 +3,7 @@ import { Component } from '@angular/core'
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ElectronService, HostAppService, ConfigService } from 'terminus-core' import { ElectronService, HostAppService, ConfigService } from 'terminus-core'
import { PasswordStorageService } from '../services/passwordStorage.service' import { PasswordStorageService } from '../services/passwordStorage.service'
import { SSHConnection, LoginScript, SSHAlgorithmType } from '../api' import { SSHConnection, LoginScript, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
import { PromptModalComponent } from './promptModal.component' import { PromptModalComponent } from './promptModal.component'
import { ALGORITHMS } from 'ssh2-streams/lib/constants' import { ALGORITHMS } from 'ssh2-streams/lib/constants'
@@ -40,18 +40,19 @@ export class EditConnectionModalComponent {
[SSHAlgorithmType.CIPHER]: 'CIPHER', [SSHAlgorithmType.CIPHER]: 'CIPHER',
[SSHAlgorithmType.HMAC]: 'HMAC', [SSHAlgorithmType.HMAC]: 'HMAC',
}[k] }[k]
this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg] this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg] this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
} }
} }
async ngOnInit () { async ngOnInit () {
this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.connection) this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.connection)
this.connection.algorithms = this.connection.algorithms || {} this.connection.algorithms = this.connection.algorithms ?? {}
this.connection.scripts = this.connection.scripts || [] this.connection.scripts = this.connection.scripts ?? []
this.connection.auth = this.connection.auth || null this.connection.auth = this.connection.auth ?? null
for (const k of Object.values(SSHAlgorithmType)) { for (const k of Object.values(SSHAlgorithmType)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!this.connection.algorithms[k]) { if (!this.connection.algorithms[k]) {
this.connection.algorithms[k] = this.defaultAlgorithms[k] this.connection.algorithms[k] = this.defaultAlgorithms[k]
} }
@@ -98,8 +99,8 @@ export class EditConnectionModalComponent {
save () { save () {
for (const k of Object.values(SSHAlgorithmType)) { for (const k of Object.values(SSHAlgorithmType)) {
this.connection.algorithms![k] = Object.entries(this.algorithms[k]) this.connection.algorithms![k] = Object.entries(this.algorithms[k])
.filter(([_k, v]) => !!v) .filter(([_, v]) => !!v)
.map(([k, _v]) => k) .map(([key, _]) => key)
} }
this.modalInstance.close(this.connection) this.modalInstance.close(this.connection)
} }

View File

@@ -127,14 +127,14 @@ export class SSHSettingsTabComponent {
this.childGroups = [] this.childGroups = []
for (const connection of this.connections) { for (const connection of this.connections) {
connection.group = connection.group || null 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 = {
name: connection.group!, name: connection.group!,
connections: [], connections: [],
} }
this.childGroups.push(group!) this.childGroups.push(group)
} }
group.connections.push(connection) group.connections.push(connection)
} }

View File

@@ -14,15 +14,17 @@ import { Subscription } from 'rxjs'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'ssh-tab', selector: 'ssh-tab',
template: BaseTerminalTabComponent.template + (require('./sshTab.component.pug') as string), template: `${BaseTerminalTabComponent.template} ${require('./sshTab.component.pug')}`,
styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles], styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class SSHTabComponent extends BaseTerminalTabComponent { export class SSHTabComponent extends BaseTerminalTabComponent {
connection: SSHConnection connection?: SSHConnection
session: SSHSession session: SSHSession|null = null
private sessionStack: SSHSession[] = [] private sessionStack: SSHSession[] = []
private homeEndSubscription: Subscription private homeEndSubscription: Subscription
private recentInputs = ''
private reconnectOffered = false
constructor ( constructor (
injector: Injector, injector: Injector,
@@ -33,6 +35,10 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
} }
ngOnInit (): void { ngOnInit (): void {
if (!this.connection) {
throw new Error('Connection not set')
}
this.logger = this.log.create('terminalTab') this.logger = this.log.create('terminalTab')
this.enableDynamicTitle = !this.connection.disableDynamicTitle this.enableDynamicTitle = !this.connection.disableDynamicTitle
@@ -53,26 +59,41 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
this.frontendReady$.pipe(first()).subscribe(() => { this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession() this.initializeSession()
this.input$.subscribe(data => {
this.recentInputs += data
this.recentInputs = this.recentInputs.substring(this.recentInputs.length - 32)
})
}) })
super.ngOnInit() super.ngOnInit()
setImmediate(() => { setImmediate(() => {
this.setTitle(this.connection.name) this.setTitle(this.connection!.name)
}) })
} }
async setupOneSession (session: SSHSession): Promise<void> { async setupOneSession (session: SSHSession): Promise<void> {
if (session.connection.jumpHost) { if (session.connection.jumpHost) {
const jumpConnection = this.config.store.ssh.connections.find(x => x.name === session.connection.jumpHost) const jumpConnection: SSHConnection|null = this.config.store.ssh.connections.find(x => x.name === session.connection.jumpHost)
if (!jumpConnection) {
throw new Error(`${session.connection.host}: jump host "${session.connection.jumpHost}" not found in your config`)
}
const jumpSession = this.ssh.createSession(jumpConnection) const jumpSession = this.ssh.createSession(jumpConnection)
await this.setupOneSession(jumpSession) await this.setupOneSession(jumpSession)
jumpSession.destroyed$.subscribe(() => session.destroy()) this.attachSessionHandler(
jumpSession.destroyed$.subscribe(() => {
if (session.open) {
session.destroy()
}
})
)
session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut( session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
'127.0.0.1', 0, session.connection.host, session.connection.port, '127.0.0.1', 0, session.connection.host, session.connection.port ?? 22,
(err, stream) => { (err, stream) => {
if (err) { if (err) {
jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`) jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
@@ -89,15 +110,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
this.sessionStack.push(session) this.sessionStack.push(session)
} }
this.attachSessionHandler(session.serviceMessage$.subscribe(msg => {
session.serviceMessage$.subscribe(msg => {
this.write(`\r\n${colors.black.bgWhite(' SSH ')} ${msg}\r\n`) this.write(`\r\n${colors.black.bgWhite(' SSH ')} ${msg}\r\n`)
session.resize(this.size.columns, this.size.rows) session.resize(this.size.columns, this.size.rows)
}) }))
session.destroyed$.subscribe(() => {
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
})
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`) this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
@@ -124,20 +141,49 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
} }
} }
protected attachSessionHandlers (): void {
const session = this.session!
super.attachSessionHandlers()
this.attachSessionHandler(session.destroyed$.subscribe(() => {
if (
// Ctrl-D
this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 ||
this.recentInputs.endsWith('exit\r')
) {
// User closed the session
this.destroy()
} else {
// Session was closed abruptly
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
if (!this.reconnectOffered) {
this.reconnectOffered = true
this.write('Press any key to reconnect\r\n')
this.attachSessionHandler(this.input$.pipe(first()).subscribe(() => {
this.reconnect()
}))
}
}
}))
}
async initializeSession (): Promise<void> { async initializeSession (): Promise<void> {
this.reconnectOffered = false
if (!this.connection) { if (!this.connection) {
this.logger.error('No SSH connection info supplied') this.logger.error('No SSH connection info supplied')
return return
} }
this.session = this.ssh.createSession(this.connection) const session = this.ssh.createSession(this.connection)
this.setSession(session)
await this.setupOneSession(this.session) try {
await this.setupOneSession(session)
} catch (e) {
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
}
this.attachSessionHandlers() await this.session!.start()
this.session!.resize(this.size.columns, this.size.rows)
await this.session.start()
this.session.resize(this.size.columns, this.size.rows)
} }
async getRecoveryToken (): Promise<RecoveryToken> { async getRecoveryToken (): Promise<RecoveryToken> {
@@ -150,27 +196,27 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
showPortForwarding (): void { showPortForwarding (): void {
const modal = this.ngbModal.open(SSHPortForwardingModalComponent).componentInstance as SSHPortForwardingModalComponent const modal = this.ngbModal.open(SSHPortForwardingModalComponent).componentInstance as SSHPortForwardingModalComponent
modal.session = this.session modal.session = this.session!
} }
async reconnect (): Promise<void> { async reconnect (): Promise<void> {
this.session?.destroy() this.session?.destroy()
await this.initializeSession() await this.initializeSession()
this.session.releaseInitialDataBuffer() this.session?.releaseInitialDataBuffer()
} }
async canClose (): Promise<boolean> { async canClose (): Promise<boolean> {
if (!this.session?.open) { if (!this.session?.open) {
return true return true
} }
if (!(this.connection.warnOnClose ?? this.config.store.ssh.warnOnClose)) { if (!(this.connection?.warnOnClose ?? this.config.store.ssh.warnOnClose)) {
return true return true
} }
return (await this.electron.showMessageBox( return (await this.electron.showMessageBox(
this.hostApp.getWindow(), this.hostApp.getWindow(),
{ {
type: 'warning', type: 'warning',
message: `Disconnect from ${this.connection.host}?`, message: `Disconnect from ${this.connection?.host}?`,
buttons: ['Cancel', 'Disconnect'], buttons: ['Cancel', 'Disconnect'],
defaultId: 1, defaultId: 1,
} }

View File

@@ -7,7 +7,7 @@ import { SSHTabComponent } from './components/sshTab.component'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> { async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
if (recoveryToken?.type === 'app:ssh-tab') { if (recoveryToken.type === 'app:ssh-tab') {
return { return {
type: SSHTabComponent, type: SSHTabComponent,
options: { options: {

View File

@@ -12,7 +12,7 @@ import * as sshpk from 'sshpk'
import { ToastrService } from 'ngx-toastr' import { ToastrService } from 'ngx-toastr'
import { HostAppService, Platform, Logger, LogService, ElectronService, AppService, SelectorOption, ConfigService } from 'terminus-core' import { HostAppService, Platform, Logger, LogService, ElectronService, AppService, SelectorOption, ConfigService } from 'terminus-core'
import { SettingsTabComponent } from 'terminus-settings' import { SettingsTabComponent } from 'terminus-settings'
import { SSHConnection, SSHSession } from '../api' import { ALGORITHM_BLACKLIST, SSHConnection, SSHSession } from '../api'
import { PromptModalComponent } from '../components/promptModal.component' import { PromptModalComponent } from '../components/promptModal.component'
import { PasswordStorageService } from './passwordStorage.service' import { PasswordStorageService } from './passwordStorage.service'
import { SSHTabComponent } from '../components/sshTab.component' import { SSHTabComponent } from '../components/sshTab.component'
@@ -98,7 +98,7 @@ export class SSHService {
} }
} }
const sshFormatKey = parsedKey!.toString('openssh') const sshFormatKey = parsedKey.toString('openssh')
const temp = await openTemp() const temp = await openTemp()
fs.close(temp.fd) fs.close(temp.fd)
await fs.writeFile(temp.path, sshFormatKey) await fs.writeFile(temp.path, sshFormatKey)
@@ -147,6 +147,10 @@ export class SSHService {
session.ssh = ssh session.ssh = ssh
let connected = false let connected = false
let savedPassword: string|null = null let savedPassword: string|null = null
const algorithms = {}
for (const key of Object.keys(session.connection.algorithms ?? {})) {
algorithms[key] = session.connection.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
}
await new Promise(async (resolve, reject) => { await new Promise(async (resolve, reject) => {
ssh.on('ready', () => { ssh.on('ready', () => {
connected = true connected = true
@@ -161,6 +165,7 @@ export class SSHService {
} }
this.zone.run(() => { this.zone.run(() => {
if (connected) { if (connected) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.toastr.error(error.toString()) this.toastr.error(error.toString())
} else { } else {
reject(error) reject(error)
@@ -209,6 +214,7 @@ export class SSHService {
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) { if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
agent = WINDOWS_OPENSSH_AGENT_PIPE agent = WINDOWS_OPENSSH_AGENT_PIPE
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-shadow
const pageantRunning = await new Promise<boolean>(resolve => { const pageantRunning = await new Promise<boolean>(resolve => {
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
resolve(list.some(x => x.name === 'pageant.exe')) resolve(list.some(x => x.name === 'pageant.exe'))
@@ -249,14 +255,14 @@ export class SSHService {
try { try {
ssh.connect({ ssh.connect({
host: session.connection.host, host: session.connection.host,
port: session.connection.port || 22, port: session.connection.port ?? 22,
username: session.connection.user, username: session.connection.user,
password: session.connection.privateKey ? undefined : '', password: session.connection.privateKey ? undefined : '',
privateKey: privateKey || undefined, privateKey: privateKey ?? undefined,
tryKeyboard: true, tryKeyboard: true,
agent: agent || undefined, agent: agent ?? undefined,
agentForward: session.connection.agentForward && !!agent, agentForward: session.connection.agentForward && !!agent,
keepaliveInterval: session.connection.keepaliveInterval, keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
keepaliveCountMax: session.connection.keepaliveCountMax, keepaliveCountMax: session.connection.keepaliveCountMax,
readyTimeout: session.connection.readyTimeout, readyTimeout: session.connection.readyTimeout,
hostVerifier: (digest: string) => { hostVerifier: (digest: string) => {
@@ -265,7 +271,7 @@ export class SSHService {
return true return true
}, },
hostHash: 'sha256' as any, hostHash: 'sha256' as any,
algorithms: session.connection.algorithms, algorithms,
sock: session.jumpStream, sock: session.jumpStream,
authHandler: methodsLeft => { authHandler: methodsLeft => {
while (true) { while (true) {
@@ -284,7 +290,7 @@ export class SSHService {
} as any) } as any)
} catch (e) { } catch (e) {
this.toastr.error(e.message) this.toastr.error(e.message)
reject(e) return reject(e)
} }
let keychainPasswordUsed = false let keychainPasswordUsed = false
@@ -358,7 +364,7 @@ export class SSHService {
name: connection.group!, name: connection.group!,
connections: [], connections: [],
} }
groups.push(group!) groups.push(group)
} }
group.connections.push(connection) group.connections.push(connection)
} }
@@ -377,7 +383,7 @@ export class SSHService {
options.push({ options.push({
name: 'Manage connections', name: 'Manage connections',
icon: 'cog', icon: 'cog',
callback: () => this.app.openNewTab(SettingsTabComponent, { activeTab: 'ssh' }), callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'ssh' }),
}) })
options.push({ options.push({
@@ -398,12 +404,10 @@ export class SSHService {
{ connection } { connection }
) as SSHTabComponent ) as SSHTabComponent
if (connection.color) { if (connection.color) {
(this.app.getParentTab(tab) || tab).color = connection.color (this.app.getParentTab(tab) ?? tab).color = connection.color
} }
setTimeout(() => { setTimeout(() => this.app.activeTab?.emitFocused())
this.app.activeTab?.emitFocused()
})
return tab return tab
} catch (error) { } catch (error) {

View File

@@ -44,14 +44,14 @@ export class WinSCPContextMenu extends TabContextMenuItemProvider {
if (!this.getPath()) { if (!this.getPath()) {
return [] return []
} }
if (!(tab instanceof SSHTabComponent)) { if (!(tab instanceof SSHTabComponent) || !tab.connection) {
return [] return []
} }
return [ return [
{ {
label: 'Launch WinSCP', label: 'Launch WinSCP',
click: (): void => { click: (): void => {
this.launchWinSCP(tab.connection) this.launchWinSCP(tab.connection!)
}, },
}, },
] ]

View File

@@ -40,7 +40,7 @@ module.exports = {
}, },
}, },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.svg/, use: ['svg-inline-loader'] }, { test: /\.svg/, use: ['svg-inline-loader'] },
], ],
}, },

View File

@@ -2,12 +2,7 @@
# yarn lockfile v1 # yarn lockfile v1
"@types/node@*": "@types/node@*", "@types/node@14.14.14":
version "14.14.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d"
integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==
"@types/node@14.14.14":
version "14.14.14" version "14.14.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==
@@ -20,9 +15,9 @@
"@types/node" "*" "@types/node" "*"
"@types/ssh2@^0.5.35": "@types/ssh2@^0.5.35":
version "0.5.45" version "0.5.46"
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.45.tgz#14e293ec2a4d4c8a5434a7989a676b87340aa870" resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584"
integrity sha512-SAQITTyO/jOoskSAw2T/9sveX4lhTzx7zdeYR0t04RMhZQrEIzvrAoCStSxYwvwZ5ofek1JWeW9x2yOK3GOUlg== integrity sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/ssh2-streams" "*" "@types/ssh2-streams" "*"

View File

@@ -27,12 +27,12 @@
"runes": "^0.4.2", "runes": "^0.4.2",
"slugify": "^1.4.0", "slugify": "^1.4.0",
"xterm": "^4.9.0-beta.7", "xterm": "^4.9.0-beta.7",
"xterm-addon-fit": "^0.4.0-beta.8", "xterm-addon-fit": "^0.5.0",
"xterm-addon-ligatures": "^0.4.0-beta.5", "xterm-addon-ligatures": "^0.4.0",
"xterm-addon-search": "^0.7.0-beta.2", "xterm-addon-search": "^0.8.0",
"xterm-addon-serialize": "^0.3.0", "xterm-addon-serialize": "^0.4.0",
"xterm-addon-unicode11": "^0.2.0", "xterm-addon-unicode11": "^0.2.0",
"xterm-addon-webgl": "^0.8.0", "xterm-addon-webgl": "^0.9.0",
"zmodem.js": "^0.1.9" "zmodem.js": "^0.1.9"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -18,6 +18,7 @@ import { TerminalDecorator } from './decorator'
/** @hidden */ /** @hidden */
export interface ToastrServiceProxy { export interface ToastrServiceProxy {
info: (_: string) => void info: (_: string) => void
error: (_: string) => void
} }
/** /**
* A class to base your custom terminal tabs on * A class to base your custom terminal tabs on
@@ -35,8 +36,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
]), ]),
])] ])]
session: BaseSession session: BaseSession|null = null
savedState: any savedState?: any
@Input() zoom = 0 @Input() zoom = 0
@@ -51,7 +52,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
/** @hidden */ /** @hidden */
@HostBinding('class.top-padded') topPadded: boolean @HostBinding('class.top-padded') topPadded: boolean
frontend: Frontend frontend?: Frontend
/** @hidden */ /** @hidden */
frontendIsReady = false frontendIsReady = false
@@ -83,7 +84,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected terminalContainersService: TerminalFrontendService protected terminalContainersService: TerminalFrontendService
protected toastr: ToastrServiceProxy protected toastr: ToastrServiceProxy
protected log: LogService protected log: LogService
protected decorators: TerminalDecorator[] protected decorators: TerminalDecorator[] = []
protected contextMenuProviders: TabContextMenuItemProvider[] protected contextMenuProviders: TabContextMenuItemProvider[]
// Deps end // Deps end
@@ -94,11 +95,31 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
private bellPlayer: HTMLAudioElement private bellPlayer: HTMLAudioElement
private termContainerSubscriptions: Subscription[] = [] private termContainerSubscriptions: Subscription[] = []
private allFocusModeSubscription: Subscription|null = null private allFocusModeSubscription: Subscription|null = null
private sessionHandlers: Subscription[] = []
get input$ (): Observable<Buffer> {
if (!this.frontend) {
throw new Error('Frontend not ready')
}
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 alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ } get resize$ (): Observable<ResizeEvent> {
if (!this.frontend) {
throw new Error('Frontend not ready')
}
return this.frontend.resize$
}
get alternateScreenActive$ (): Observable<boolean> {
if (!this.frontend) {
throw new Error('Frontend not ready')
}
return this.frontend.alternateScreenActive$
}
get frontendReady$ (): Observable<void> { return this.frontendReady } get frontendReady$ (): Observable<void> { return this.frontendReady }
constructor (protected injector: Injector) { constructor (protected injector: Injector) {
@@ -119,16 +140,15 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.contextMenuProviders = injector.get<any>(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[] this.contextMenuProviders = injector.get<any>(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[]
this.logger = this.log.create('baseTerminalTab') this.logger = this.log.create('baseTerminalTab')
this.decorators = this.decorators || []
this.setTitle('Terminal') this.setTitle('Terminal')
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(async hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
return return
} }
switch (hotkey) { switch (hotkey) {
case 'ctrl-c': case 'ctrl-c':
if (this.frontend.getSelection()) { if (this.frontend?.getSelection()) {
this.frontend.copySelection() this.frontend.copySelection()
this.frontend.clearSelection() this.frontend.clearSelection()
this.toastr.info('Copied') this.toastr.info('Copied')
@@ -137,15 +157,15 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
break break
case 'copy': case 'copy':
this.frontend.copySelection() this.frontend?.copySelection()
this.frontend.clearSelection() this.frontend?.clearSelection()
this.toastr.info('Copied') this.toastr.info('Copied')
break break
case 'paste': case 'paste':
this.paste() this.paste()
break break
case 'clear': case 'clear':
this.frontend.clear() this.frontend?.clear()
break break
case 'zoom-in': case 'zoom-in':
this.zoomIn() this.zoomIn()
@@ -189,6 +209,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
case 'pane-focus-all': case 'pane-focus-all':
this.focusAllPanes() this.focusAllPanes()
break break
case 'copy-current-path':
this.copyCurrentPath()
break
} }
}) })
this.bellPlayer = document.createElement('audio') this.bellPlayer = document.createElement('audio')
@@ -201,7 +224,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
ngOnInit (): void { ngOnInit (): void {
this.focused$.subscribe(() => { this.focused$.subscribe(() => {
this.configure() this.configure()
this.frontend.focus() this.frontend?.focus()
}) })
this.frontend = this.terminalContainersService.getFrontend(this.session) this.frontend = this.terminalContainersService.getFrontend(this.session)
@@ -223,10 +246,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}) })
setTimeout(() => { setTimeout(() => {
this.session.resize(columns, rows) this.session?.resize(columns, rows)
}, 1000) }, 1000)
this.session.releaseInitialDataBuffer() this.session?.releaseInitialDataBuffer()
}) })
this.alternateScreenActive$.subscribe(x => { this.alternateScreenActive$.subscribe(x => {
@@ -242,12 +265,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
setImmediate(() => { setImmediate(() => {
if (this.hasFocus) { if (this.hasFocus) {
this.frontend.attach(this.content.nativeElement) this.frontend!.attach(this.content.nativeElement)
this.frontend.configure() this.frontend!.configure()
} else { } else {
this.focused$.pipe(first()).subscribe(() => { this.focused$.pipe(first()).subscribe(() => {
this.frontend.attach(this.content.nativeElement) this.frontend!.attach(this.content.nativeElement)
this.frontend.configure() this.frontend!.configure()
}) })
} }
}) })
@@ -264,7 +287,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.frontend.bell$.subscribe(() => { this.frontend.bell$.subscribe(() => {
if (this.config.store.terminal.bell === 'visual') { if (this.config.store.terminal.bell === 'visual') {
this.frontend.visualBell() this.frontend?.visualBell()
} }
if (this.config.store.terminal.bell === 'audible') { if (this.config.store.terminal.bell === 'audible') {
this.bellPlayer.play() this.bellPlayer.play()
@@ -295,9 +318,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
if (!(data instanceof Buffer)) { if (!(data instanceof Buffer)) {
data = Buffer.from(data, 'utf-8') 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()
} }
} }
@@ -305,6 +328,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
* Feeds input into the terminal frontend * Feeds input into the terminal frontend
*/ */
write (data: string): void { write (data: string): void {
if (!this.frontend) {
throw new Error('Frontend not ready')
}
const percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data) const percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
if (!this.alternateScreenActive && percentageMatch) { if (!this.alternateScreenActive && percentageMatch) {
const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2]) const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
@@ -321,7 +348,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
async paste (): Promise<void> { async paste (): Promise<void> {
let data = this.electron.clipboard.readText() let data = this.electron.clipboard.readText()
if (this.config.store.terminal.bracketedPaste) { if (this.config.store.terminal.bracketedPaste) {
data = '\x1b[200~' + data + '\x1b[201~' data = `\x1b[200~${data}\x1b[201~`
} }
if (this.hostApp.platform === Platform.Windows) { if (this.hostApp.platform === Platform.Windows) {
data = data.replace(/\r\n/g, '\r') data = data.replace(/\r\n/g, '\r')
@@ -357,7 +384,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
* Applies the user settings to the terminal * Applies the user settings to the terminal
*/ */
configure (): void { configure (): void {
this.frontend.configure() this.frontend?.configure()
this.topPadded = this.hostApp.platform === Platform.macOS this.topPadded = this.hostApp.platform === Platform.macOS
&& this.config.store.appearance.frame === 'thin' && this.config.store.appearance.frame === 'thin'
@@ -374,17 +401,17 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
zoomIn (): void { zoomIn (): void {
this.zoom++ this.zoom++
this.frontend.setZoom(this.zoom) this.frontend?.setZoom(this.zoom)
} }
zoomOut (): void { zoomOut (): void {
this.zoom-- this.zoom--
this.frontend.setZoom(this.zoom) this.frontend?.setZoom(this.zoom)
} }
resetZoom (): void { resetZoom (): void {
this.zoom = 0 this.zoom = 0
this.frontend.setZoom(this.zoom) this.frontend?.setZoom(this.zoom)
} }
focusAllPanes (): void { focusAllPanes (): void {
@@ -392,15 +419,16 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
return return
} }
if (this.parent instanceof SplitTabComponent) { if (this.parent instanceof SplitTabComponent) {
this.parent._allFocusMode = true const parent = this.parent
this.parent.layout() parent._allFocusMode = true
this.allFocusModeSubscription = this.frontend.input$.subscribe(data => { parent.layout()
for (const tab of (this.parent as SplitTabComponent).getAllTabs()) { this.allFocusModeSubscription = this.frontend?.input$.subscribe(data => {
for (const tab of parent.getAllTabs()) {
if (tab !== this && tab instanceof BaseTerminalTabComponent) { if (tab !== this && tab instanceof BaseTerminalTabComponent) {
tab.sendInput(data) tab.sendInput(data)
} }
} }
}) }) ?? null
} }
} }
@@ -416,9 +444,22 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
} }
async copyCurrentPath (): Promise<void> {
let cwd: string|null = null
if (this.session?.supportsWorkingDirectory()) {
cwd = await this.session.getWorkingDirectory()
}
if (cwd) {
this.electron.clipboard.writeText(cwd)
this.toastr.info('Copied')
} else {
this.toastr.error('Shell does not support current path detection')
}
}
/** @hidden */ /** @hidden */
ngOnDestroy (): void { ngOnDestroy (): void {
this.frontend.detach(this.content.nativeElement) this.frontend?.detach(this.content.nativeElement)
this.detachTermContainerHandlers() this.detachTermContainerHandlers()
this.config.enabledServices(this.decorators).forEach(decorator => { this.config.enabledServices(this.decorators).forEach(decorator => {
try { try {
@@ -451,6 +492,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected attachTermContainerHandlers (): void { protected attachTermContainerHandlers (): void {
this.detachTermContainerHandlers() this.detachTermContainerHandlers()
if (!this.frontend) {
throw new Error('Frontend not ready')
}
const maybeConfigure = () => { const maybeConfigure = () => {
if (this.hasFocus) { if (this.hasFocus) {
setTimeout(() => this.configure(), 250) setTimeout(() => this.configure(), 250)
@@ -464,8 +509,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
})), })),
this.focused$.subscribe(() => this.frontend.enableResizing = true), this.focused$.subscribe(() => this.frontend && (this.frontend.enableResizing = true)),
this.blurred$.subscribe(() => this.frontend.enableResizing = false), this.blurred$.subscribe(() => this.frontend && (this.frontend.enableResizing = false)),
this.frontend.mouseEvent$.subscribe(async event => { this.frontend.mouseEvent$.subscribe(async event => {
if (event.type === 'mousedown') { if (event.type === 'mousedown') {
@@ -524,22 +569,55 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
] ]
} }
setSession (session: BaseSession|null, destroyOnSessionClose = false): void {
if (session) {
if (this.session) {
this.setSession(null)
}
this.detachSessionHandlers()
this.session = session
this.attachSessionHandlers(destroyOnSessionClose)
} else {
this.detachSessionHandlers()
this.session = null
}
}
protected attachSessionHandler (subscription: Subscription): void {
this.sessionHandlers.push(subscription)
}
protected attachSessionHandlers (destroyOnSessionClose = false): void { protected attachSessionHandlers (destroyOnSessionClose = false): void {
if (!this.session) {
throw new Error('Session not set')
}
// this.session.output$.bufferTime(10).subscribe((datas) => { // this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => { this.attachSessionHandler(this.session.output$.subscribe(data => {
if (this.enablePassthrough) { if (this.enablePassthrough) {
this.zone.run(() => { this.zone.run(() => {
this.output.next(data) this.output.next(data)
this.write(data) this.write(data)
}) })
} }
}) }))
if (destroyOnSessionClose) { if (destroyOnSessionClose) {
this.sessionCloseSubscription = this.session.closed$.subscribe(() => { this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.frontend.destroy() this.frontend?.destroy()
this.destroy() this.destroy()
}) }))
} }
this.attachSessionHandler(this.session.destroyed$.subscribe(() => {
this.setSession(null)
}))
}
protected detachSessionHandlers (): void {
for (const s of this.sessionHandlers) {
s.unsubscribe()
}
this.sessionHandlers = []
} }
} }

View File

@@ -17,7 +17,7 @@ export abstract class TerminalDecorator {
* Make sure to call super() * Make sure to call super()
*/ */
detach (terminal: BaseTerminalTabComponent): void { detach (terminal: BaseTerminalTabComponent): void {
for (const s of this.smartSubscriptions.get(terminal) || []) { for (const s of this.smartSubscriptions.get(terminal) ?? []) {
s.unsubscribe() s.unsubscribe()
} }
this.smartSubscriptions.delete(terminal) this.smartSubscriptions.delete(terminal)
@@ -26,7 +26,10 @@ export abstract class TerminalDecorator {
/** /**
* Automatically cancel @subscription once detached from @terminal * Automatically cancel @subscription once detached from @terminal
*/ */
protected subscribeUntilDetached (terminal: BaseTerminalTabComponent, subscription: Subscription): void { protected subscribeUntilDetached (terminal: BaseTerminalTabComponent, subscription?: Subscription): void {
if (!subscription) {
return
}
if (!this.smartSubscriptions.has(terminal)) { if (!this.smartSubscriptions.has(terminal)) {
this.smartSubscriptions.set(terminal, []) this.smartSubscriptions.set(terminal, [])
} }

View File

@@ -6,7 +6,7 @@ export interface ResizeEvent {
export interface SessionOptions { export interface SessionOptions {
name?: string name?: string
command: string command: string
args: string[] args?: string[]
cwd?: string cwd?: string
env?: Record<string, string> env?: Record<string, string>
width?: number width?: number

View File

@@ -93,11 +93,11 @@ export class ColorSchemeSettingsTabComponent {
} }
getCurrentSchemeName () { getCurrentSchemeName () {
return (this.currentCustomScheme || this.currentStockScheme)?.name || 'Custom' return (this.currentCustomScheme ?? this.currentStockScheme)?.name ?? 'Custom'
} }
findMatchingScheme (scheme: TerminalColorScheme, schemes: TerminalColorScheme[]) { findMatchingScheme (scheme: TerminalColorScheme, schemes: TerminalColorScheme[]) {
return schemes.find(x => deepEqual(x, scheme)) || null return schemes.find(x => deepEqual(x, scheme)) ?? null
} }
colorsTrackBy (index) { colorsTrackBy (index) {

View File

@@ -18,8 +18,8 @@ export class EditProfileModalComponent {
} }
ngOnInit () { ngOnInit () {
this.profile.sessionOptions.env = this.profile.sessionOptions.env || {} this.profile.sessionOptions.env = this.profile.sessionOptions.env ?? {}
this.profile.sessionOptions.args = this.profile.sessionOptions.args || [] this.profile.sessionOptions.args = this.profile.sessionOptions.args ?? []
} }
save () { save () {

View File

@@ -10,7 +10,7 @@ import { Subject } from 'rxjs'
}) })
export class EnvironmentEditorComponent { export class EnvironmentEditorComponent {
@Output() modelChange = new Subject<any>() @Output() modelChange = new Subject<any>()
vars: {key: string, value: string}[] = [] vars: { key: string, value: string }[] = []
private cachedModel: any private cachedModel: any
@Input() get model (): any { @Input() get model (): any {

View File

@@ -57,15 +57,14 @@ h3.mb-3 Shell
.form-line .form-line
.header .header
.title Always Use Working Directory .title Directory for new tabs
.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( select.form-control(
[(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory', [(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory',
(ngModelChange)='config.save()' (ngModelChange)='config.save()',
) )
option([ngValue]='false') Same as active tab's directory
option([ngValue]='true') The working directory from above
.form-line.align-items-start .form-line.align-items-start
.header .header
@@ -97,7 +96,7 @@ h3.mt-3 Saved Profiles
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') .pb-4(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

View File

@@ -60,14 +60,12 @@ export class ShellSettingsTabComponent {
properties: ['openDirectory', 'showHiddenFiles'], properties: ['openDirectory', 'showHiddenFiles'],
} }
)).filePaths )).filePaths
if (paths) { this.config.store.terminal.workingDirectory = paths[0]
this.config.store.terminal.workingDirectory = paths[0]
}
} }
newProfile (shell: Shell): void { newProfile (shell: Shell): void {
const profile: Profile = { const profile: Profile = {
name: shell.name || '', name: shell.name ?? '',
shell: shell.id, shell: shell.id,
sessionOptions: this.terminal.optionsFromShell(shell), sessionOptions: this.terminal.optionsFromShell(shell),
} }

View File

@@ -53,7 +53,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
initializeSession (columns: number, rows: number): void { initializeSession (columns: number, rows: number): void {
this.sessions.addSession( this.sessions.addSession(
this.session, this.session!,
Object.assign({}, this.sessionOptions, { Object.assign({}, this.sessionOptions, {
width: columns, width: columns,
height: rows, height: rows,
@@ -69,15 +69,15 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
type: 'app:terminal-tab', type: 'app:terminal-tab',
sessionOptions: { sessionOptions: {
...this.sessionOptions, ...this.sessionOptions,
cwd: cwd || this.sessionOptions.cwd, cwd: cwd ?? this.sessionOptions.cwd,
}, },
savedState: this.frontend?.saveState(), savedState: this.frontend?.saveState(),
} }
} }
async getCurrentProcess (): Promise<BaseTabProcess|null> { async getCurrentProcess (): Promise<BaseTabProcess|null> {
const children = await this.session.getChildProcesses() const children = await this.session?.getChildProcesses()
if (!children.length) { if (!children?.length) {
return null return null
} }
return { return {
@@ -86,8 +86,8 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
} }
async canClose (): Promise<boolean> { async canClose (): Promise<boolean> {
const children = await this.session.getChildProcesses() const children = await this.session?.getChildProcesses()
if (children.length === 0) { if (!children?.length) {
return true return true
} }
return (await this.electron.showMessageBox( return (await this.electron.showMessageBox(
@@ -104,6 +104,6 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
ngOnDestroy (): void { ngOnDestroy (): void {
this.homeEndSubscription.unsubscribe() this.homeEndSubscription.unsubscribe()
super.ngOnDestroy() super.ngOnDestroy()
this.session.destroy() this.session?.destroy()
} }
} }

View File

@@ -4,6 +4,7 @@ import { ConfigProvider, Platform } from 'terminus-core'
export class TerminalConfigProvider extends ConfigProvider { export class TerminalConfigProvider extends ConfigProvider {
defaults = { defaults = {
hotkeys: { hotkeys: {
'copy-current-path': [],
shell: { shell: {
__nonStructural: true, __nonStructural: true,
}, },

View File

@@ -18,7 +18,7 @@ export class DebugDecorator extends TerminalDecorator {
let sessionOutputBuffer = '' let sessionOutputBuffer = ''
const bufferLength = 8192 const bufferLength = 8192
this.subscribeUntilDetached(terminal, terminal.session.output$.subscribe(data => { this.subscribeUntilDetached(terminal, terminal.session!.output$.subscribe(data => {
sessionOutputBuffer += data sessionOutputBuffer += data
if (sessionOutputBuffer.length > bufferLength) { if (sessionOutputBuffer.length > bufferLength) {
sessionOutputBuffer = sessionOutputBuffer.substring(sessionOutputBuffer.length - bufferLength) sessionOutputBuffer = sessionOutputBuffer.substring(sessionOutputBuffer.length - bufferLength)
@@ -88,18 +88,18 @@ export class DebugDecorator extends TerminalDecorator {
} }
private doSaveState (terminal: TerminalTabComponent) { private doSaveState (terminal: TerminalTabComponent) {
this.saveFile(terminal.frontend.saveState(), 'state.txt') this.saveFile(terminal.frontend!.saveState(), 'state.txt')
} }
private async doCopyState (terminal: TerminalTabComponent) { private async doCopyState (terminal: TerminalTabComponent) {
const data = '```' + JSON.stringify(terminal.frontend.saveState()) + '```' const data = '```' + JSON.stringify(terminal.frontend!.saveState()) + '```'
this.electron.clipboard.writeText(data) this.electron.clipboard.writeText(data)
} }
private async doLoadState (terminal: TerminalTabComponent) { private async doLoadState (terminal: TerminalTabComponent) {
const data = await this.loadFile() const data = await this.loadFile()
if (data) { if (data) {
terminal.frontend.restoreState(data) terminal.frontend!.restoreState(data)
} }
} }
@@ -109,7 +109,7 @@ export class DebugDecorator extends TerminalDecorator {
if (data.startsWith('`')) { if (data.startsWith('`')) {
data = data.substring(3, data.length - 3) data = data.substring(3, data.length - 3)
} }
terminal.frontend.restoreState(JSON.parse(data)) terminal.frontend!.restoreState(JSON.parse(data))
} }
} }
@@ -125,7 +125,7 @@ export class DebugDecorator extends TerminalDecorator {
private async doLoadOutput (terminal: TerminalTabComponent) { private async doLoadOutput (terminal: TerminalTabComponent) {
const data = await this.loadFile() const data = await this.loadFile()
if (data) { if (data) {
terminal.frontend.write(data) terminal.frontend?.write(data)
} }
} }
@@ -135,7 +135,7 @@ export class DebugDecorator extends TerminalDecorator {
if (data.startsWith('`')) { if (data.startsWith('`')) {
data = data.substring(3, data.length - 3) data = data.substring(3, data.length - 3)
} }
terminal.frontend.write(JSON.parse(data)) terminal.frontend?.write(JSON.parse(data))
} }
} }
} }

View File

@@ -7,10 +7,10 @@ import { TerminalTabComponent } from '../components/terminalTab.component'
export class PathDropDecorator extends TerminalDecorator { export class PathDropDecorator extends TerminalDecorator {
attach (terminal: TerminalTabComponent): void { attach (terminal: TerminalTabComponent): void {
setTimeout(() => { setTimeout(() => {
this.subscribeUntilDetached(terminal, terminal.frontend.dragOver$.subscribe(event => { this.subscribeUntilDetached(terminal, terminal.frontend?.dragOver$.subscribe(event => {
event.preventDefault() event.preventDefault()
})) }))
this.subscribeUntilDetached(terminal, terminal.frontend.drop$.subscribe(event => { this.subscribeUntilDetached(terminal, terminal.frontend?.drop$.subscribe((event: DragEvent) => {
for (const file of event.dataTransfer!.files as any) { for (const file of event.dataTransfer!.files as any) {
this.injectPath(terminal, file.path) this.injectPath(terminal, file.path)
} }

View File

@@ -36,7 +36,7 @@ export class ZModemDecorator extends TerminalDecorator {
terminal.write(data) terminal.write(data)
} }
}, },
sender: data => terminal.session.write(Buffer.from(data)), sender: data => terminal.session!.write(Buffer.from(data)),
on_detect: async detection => { on_detect: async detection => {
try { try {
terminal.enablePassthrough = false terminal.enablePassthrough = false
@@ -50,7 +50,7 @@ export class ZModemDecorator extends TerminalDecorator {
}, },
}) })
setTimeout(() => { setTimeout(() => {
this.subscribeUntilDetached(terminal, terminal.session.binaryOutput$.subscribe(data => { this.subscribeUntilDetached(terminal, terminal.session!.binaryOutput$.subscribe(data => {
const chunkSize = 1024 const chunkSize = 1024
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) { for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
try { try {
@@ -153,6 +153,7 @@ export class ZModemDecorator extends TerminalDecorator {
this.cancelEvent.toPromise(), this.cancelEvent.toPromise(),
]) ])
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (canceled) { if (canceled) {
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name) this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name)
} else { } else {
@@ -207,6 +208,7 @@ export class ZModemDecorator extends TerminalDecorator {
await xfer.end() await xfer.end()
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (canceled) { if (canceled) {
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + offer.name) this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + offer.name)
} else { } else {

View File

@@ -33,7 +33,7 @@ export class XTermFrontend extends Frontend {
private search = new SearchAddon() private search = new SearchAddon()
private fitAddon = new FitAddon() private fitAddon = new FitAddon()
private serializeAddon = new SerializeAddon() private serializeAddon = new SerializeAddon()
private ligaturesAddon: LigaturesAddon private ligaturesAddon?: LigaturesAddon
private opened = false private opened = false
constructor () { constructor () {
@@ -298,7 +298,7 @@ export class XTermFrontend extends Frontend {
html += this.getLineAsHTML(selection.startRow, selection.startColumn, selection.endColumn) html += this.getLineAsHTML(selection.startRow, selection.startColumn, selection.endColumn)
} else { } else {
html += this.getLineAsHTML(selection.startRow, selection.startColumn, this.xterm.cols) html += this.getLineAsHTML(selection.startRow, selection.startColumn, this.xterm.cols)
for (let y = selection.startRow! + 1; y < selection.endRow; y++) { for (let y = selection.startRow + 1; y < selection.endRow; y++) {
html += this.getLineAsHTML(y, 0, this.xterm.cols) html += this.getLineAsHTML(y, 0, this.xterm.cols)
} }
html += this.getLineAsHTML(selection.endRow, 0, selection.endColumn) html += this.getLineAsHTML(selection.endRow, 0, selection.endColumn)

View File

@@ -62,6 +62,10 @@ export class TerminalHotkeyProvider extends HotkeyProvider {
id: 'ctrl-c', id: 'ctrl-c',
name: 'Intelligent Ctrl-C (copy/abort)', name: 'Intelligent Ctrl-C (copy/abort)',
}, },
{
id: 'copy-current-path',
name: 'Copy current path',
},
{ {
id: 'search', id: 'search',
name: 'Search', name: 'Search',

View File

@@ -171,7 +171,7 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
argv = argv.slice(1) argv = argv.slice(1)
} }
if (require('yargs').parse(argv.slice(1))._[0] !== 'open'){ if (require('yargs/yargs')(argv.slice(1)).parse()._[0] !== 'open'){
app.ready$.subscribe(() => { app.ready$.subscribe(() => {
terminal.openTab() terminal.openTab()
}) })
@@ -194,6 +194,9 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
}) })
hostApp.cliOpenDirectory$.subscribe(async directory => { hostApp.cliOpenDirectory$.subscribe(async directory => {
if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) {
directory = directory.substring(0, directory.length - 1)
}
if (await fs.exists(directory)) { if (await fs.exists(directory)) {
if ((await fs.stat(directory)).isDirectory()) { if ((await fs.stat(directory)).isDirectory()) {
terminal.openTab(undefined, directory) terminal.openTab(undefined, directory)

View File

@@ -7,7 +7,7 @@ import { TerminalTabComponent } from './components/terminalTab.component'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> { async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> {
if (recoveryToken?.type === 'app:terminal-tab') { if (recoveryToken.type === 'app:terminal-tab') {
return { return {
type: TerminalTabComponent, type: TerminalTabComponent,
options: { options: {

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