Compare commits

...

102 Commits

Author SHA1 Message Date
Eugene Pankov
0f6855d978 only leave a process running on macOS- fixes #4442 2021-08-19 09:35:42 +02:00
Eugene Pankov
3102f39706 Update yarn.lock 2021-08-18 23:57:58 +02:00
Eugene Pankov
46ac0a6caf Update yarn.lock 2021-08-18 23:56:23 +02:00
Eugene Pankov
68e1db040a bumped node-pty 2021-08-18 23:56:20 +02:00
Eugene Pankov
e34772b8b8 throw build errors in CI 2021-08-18 23:29:35 +02:00
Eugene Pankov
cf6558ec6a lint 2021-08-16 20:11:22 +02:00
Eugene Pankov
f5f88d3d9d fixed touchbar activity indicators 2021-08-16 20:10:34 +02:00
Eugene Pankov
64955bfcd6 fixed macos-release imports 2021-08-16 20:10:30 +02:00
Eugene Pankov
9fa9021a81 fixed xterm cursor blinker leak - fixes #4166 2021-08-16 20:10:22 +02:00
Eugene Pankov
43183401b7 dropped nested corejs dep 2021-08-16 20:10:15 +02:00
dependabot[bot]
880b9ce82b Bump macos-release from 2.5.0 to 3.0.0
Bumps [macos-release](https://github.com/sindresorhus/macos-release) from 2.5.0 to 3.0.0.
- [Release notes](https://github.com/sindresorhus/macos-release/releases)
- [Commits](https://github.com/sindresorhus/macos-release/compare/v2.5.0...v3.0.0)

---
updated-dependencies:
- dependency-name: macos-release
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-16 20:10:04 +02:00
Eugene Pankov
3584af524b telnet: added potentially missing WILL responses 2021-08-15 22:31:40 +02:00
Eugene Pankov
af174933d6 telnet: fixed size negotiation order 2021-08-15 19:58:28 +02:00
Eugene Pankov
c4490717c0 lint 2021-08-15 19:42:26 +02:00
Eugene Pankov
70b6be7301 autocomplete profile colors - fixes #4231 2021-08-15 16:16:26 +02:00
Eugene Pankov
8d5b0fe863 macOS: don't exit when the last window is closed - fixes #4263 2021-08-15 14:42:48 +02:00
Eugene Pankov
03045eb952 fixed titlebar not adjusting to macOS fullscreen mode - fixes #4274 2021-08-15 14:40:40 +02:00
Eugene Pankov
f7b0272be5 allow type aliases 2021-08-15 14:23:26 +02:00
Eugene Pankov
4fa16c8a20 fixed split attachment - fixes #4374 2021-08-15 14:13:29 +02:00
Eugene Pankov
855a7bbe14 include platform arch in github issues 2021-08-15 13:47:56 +02:00
Eugene Pankov
2b3694f517 theme tweaks 2021-08-15 13:47:46 +02:00
Eugene Pankov
101177a865 fixed jump hosts using a partial profile - fixes #3294, fixes #4307, fixes #4396 2021-08-15 13:38:11 +02:00
Eugene Pankov
8b33f98c79 reduce hotkey timeout 2021-08-15 13:25:14 +02:00
Eugene Pankov
98e52f50a9 lint 2021-08-14 23:37:42 +02:00
Eugene Pankov
7551201796 fixed recovery of custom tab titles - fixes #4406 2021-08-14 23:36:41 +02:00
Eugene Pankov
3fe2dccb94 fixed instantiating saved layouts - fixes #4413 2021-08-14 23:34:01 +02:00
Eugene Pankov
f53eb31274 theme tweaks 2021-08-14 23:27:13 +02:00
Eugene Pankov
81663f351a show icon colors in profile selector - fixes #4405 2021-08-14 23:14:14 +02:00
Eugeny
bf5d037cff Merge pull request #4385 from Eugeny/dependabot/npm_and_yarn/electron-13.1.9
Bump electron from 13.1.8 to 13.1.9
2021-08-11 10:52:47 +02:00
Eugeny
53d9af3279 Merge pull request #4386 from Eugeny/dependabot/npm_and_yarn/electron-rebuild-3.1.1
Bump electron-rebuild from 2.3.5 to 3.1.1
2021-08-11 10:52:33 +02:00
Eugeny
b7dd354313 Merge pull request #4394 from Eugeny/dependabot/npm_and_yarn/webpack-5.50.0
Bump webpack from 5.48.0 to 5.50.0
2021-08-11 10:52:14 +02:00
dependabot[bot]
d8bc9ce859 Bump webpack from 5.48.0 to 5.50.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.48.0 to 5.50.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.48.0...v5.50.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-11 04:05:45 +00:00
dependabot[bot]
1bb9358f77 Bump electron-rebuild from 2.3.5 to 3.1.1
Bumps [electron-rebuild](https://github.com/electron/electron-rebuild) from 2.3.5 to 3.1.1.
- [Release notes](https://github.com/electron/electron-rebuild/releases)
- [Changelog](https://github.com/electron/electron-rebuild/blob/master/.releaserc.json)
- [Commits](https://github.com/electron/electron-rebuild/compare/v2.3.5...v3.1.1)

---
updated-dependencies:
- dependency-name: electron-rebuild
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 04:05:35 +00:00
dependabot[bot]
fa77ff3995 Bump electron from 13.1.8 to 13.1.9
Bumps [electron](https://github.com/electron/electron) from 13.1.8 to 13.1.9.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v13.1.8...v13.1.9)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 04:04:55 +00:00
Eugene Pankov
1ae8d9c643 removed hterm - #4295 2021-08-08 22:06:08 +02:00
Eugene Pankov
a560f0c96e Update README.md 2021-08-08 18:04:21 +02:00
Eugene Pankov
434bacf185 fixed hotkey race condition 2021-08-08 17:54:53 +02:00
Eugene Pankov
79de7ec015 selector ui tweaks 2021-08-08 17:54:39 +02:00
Eugene Pankov
dfdb3b051b fixed hotkey IDs for profiles with "." in name - fixes #4367 2021-08-07 19:35:11 +02:00
Eugene Pankov
9fbf9136fc moved touchbar handling into main process 2021-08-07 19:34:37 +02:00
Eugene Pankov
25fdba7104 throttle global hotkey - fixes #4371 2021-08-07 10:25:49 +02:00
Eugeny
c91707e94f Merge pull request #4331 from Eugeny/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-4.29.0
Bump @typescript-eslint/eslint-plugin from 4.28.5 to 4.29.0
2021-08-06 10:12:53 +02:00
Eugeny
d665eef430 Merge pull request #4349 from Eugeny/dependabot/npm_and_yarn/app/yargs-17.1.0
Bump yargs from 17.0.1 to 17.1.0 in /app
2021-08-06 10:12:42 +02:00
Eugeny
4579e839cd Merge pull request #4350 from Eugeny/dependabot/npm_and_yarn/app/angular/cdk-12.2.0
Bump @angular/cdk from 12.1.2 to 12.2.0 in /app
2021-08-06 10:12:14 +02:00
Eugeny
6e952180ec Merge pull request #4351 from Eugeny/dependabot/npm_and_yarn/fortawesome/fontawesome-free-5.15.4
Bump @fortawesome/fontawesome-free from 5.15.3 to 5.15.4
2021-08-06 10:12:04 +02:00
Eugeny
a947254ca8 Merge pull request #4360 from Eugeny/dependabot/npm_and_yarn/graceful-fs-4.2.8
Bump graceful-fs from 4.2.6 to 4.2.8
2021-08-06 10:11:05 +02:00
Eugeny
1eb4a7fc26 Merge pull request #4361 from Eugeny/dependabot/github_actions/actions/setup-node-2.4.0
Bump actions/setup-node from 2.2.0 to 2.4.0
2021-08-06 10:10:48 +02:00
Eugeny
78f25a7679 Merge pull request #4364 from Eugeny/all-contributors/add-al-wi 2021-08-06 10:09:35 +02:00
allcontributors[bot]
0c4d8b0784 docs: update .all-contributorsrc [skip ci] 2021-08-06 08:09:22 +00:00
Eugeny
e2c8093b97 Merge pull request #4363 from al-wi/profile-filter-terms
Profile filter per search term instead of the entire search input
2021-08-06 10:09:21 +02:00
allcontributors[bot]
c497a71361 docs: update README.md [skip ci] 2021-08-06 08:09:21 +00:00
Alexander Wiedemann
ec2982b1c4 Filter per search term instead of the entire search input 2021-08-06 09:55:15 +02:00
Eugene Pankov
be0aeefdb3 Update baseTerminalTab.component.ts 2021-08-06 09:52:37 +02:00
Eugene Pankov
eadd8d563e more hotkey fixes 2021-08-06 09:52:34 +02:00
Eugene Pankov
08f1ad4c75 hotkey handling fixes 2021-08-06 09:43:54 +02:00
Eugene Pankov
426606ba06 fixed split tab index - fixes #4249 2021-08-06 09:07:38 +02:00
Eugene Pankov
7b59ba4b73 reworked hotkey handling - fixes #4355, fixes #4326, fixes #4340 2021-08-06 09:03:55 +02:00
dependabot[bot]
0471fcec15 Bump actions/setup-node from 2.2.0 to 2.4.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.2.0...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-06 04:19:59 +00:00
dependabot[bot]
4110d09dab Bump graceful-fs from 4.2.6 to 4.2.8
Bumps [graceful-fs](https://github.com/isaacs/node-graceful-fs) from 4.2.6 to 4.2.8.
- [Release notes](https://github.com/isaacs/node-graceful-fs/releases)
- [Commits](https://github.com/isaacs/node-graceful-fs/compare/v4.2.6...v4.2.8)

---
updated-dependencies:
- dependency-name: graceful-fs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-06 04:07:47 +00:00
Eugene Pankov
533837f5b7 another possible fix for profile editing - #4330 2021-08-05 10:44:00 +02:00
dependabot[bot]
144924e579 Bump @fortawesome/fontawesome-free from 5.15.3 to 5.15.4
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 5.15.3 to 5.15.4.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/5.15.3...5.15.4)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-05 04:03:37 +00:00
dependabot[bot]
6902ccdb95 Bump @angular/cdk from 12.1.2 to 12.2.0 in /app
Bumps [@angular/cdk](https://github.com/angular/components) from 12.1.2 to 12.2.0.
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/12.1.2...12.2.0)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-05 04:01:54 +00:00
dependabot[bot]
7ed5aff168 Bump yargs from 17.0.1 to 17.1.0 in /app
Bumps [yargs](https://github.com/yargs/yargs) from 17.0.1 to 17.1.0.
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/compare/v17.0.1...v17.1.0)

---
updated-dependencies:
- dependency-name: yargs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-05 04:01:42 +00:00
Eugene Pankov
3f0db97a68 finally fixed docs 2021-08-04 20:53:42 +02:00
Eugene Pankov
231594d709 fixed docs build 2021-08-04 20:28:22 +02:00
Eugene Pankov
e4ae114c71 removed old ssh2 patch 2021-08-04 20:22:39 +02:00
Eugene Pankov
20000d16f8 fixed build order 2021-08-04 20:21:14 +02:00
Eugene Pankov
5e0a9b2e52 Merge branch 'master' of github.com:Eugeny/tabby 2021-08-04 19:51:11 +02:00
Eugene Pankov
fa70447223 keep "disable dynamic title" while duplicating or restoring a tab - fixes #4334 2021-08-04 19:51:08 +02:00
dependabot[bot]
acf418b52f Bump @typescript-eslint/eslint-plugin from 4.28.5 to 4.29.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.28.5 to 4.29.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.29.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-04 17:45:36 +00:00
Eugene Pankov
28b84e38ca fixed profiles not getting saved on Windows - fixes #4330 2021-08-04 19:44:02 +02:00
Eugeny
3c4a078fa5 Merge pull request #4322 from Eugeny/dependabot/npm_and_yarn/typedoc-0.21.5
Bump typedoc from 0.21.4 to 0.21.5
2021-08-04 19:41:21 +02:00
Eugeny
52f4e88420 Merge pull request #4333 from Eugeny/dependabot/npm_and_yarn/webpack-5.48.0 2021-08-04 19:40:47 +02:00
Eugeny
16d9045a80 Merge pull request #4336 from Eugeny/dependabot/npm_and_yarn/electron-13.1.8
Bump electron from 13.1.7 to 13.1.8
2021-08-04 19:39:37 +02:00
Eugeny
07d7d8daba Merge pull request #4321 from Eugeny/dependabot/npm_and_yarn/electron-notarize-1.0.1 2021-08-04 19:39:16 +02:00
Eugeny
b2b9476298 Merge pull request #4320 from Eugeny/dependabot/npm_and_yarn/eslint-7.32.0
Bump eslint from 7.31.0 to 7.32.0
2021-08-04 19:38:48 +02:00
Eugeny
cf7f3dffe3 Merge pull request #4317 from Eugeny/dependabot/npm_and_yarn/ssh2-1.2.0
Bump ssh2 from 1.1.0 to 1.2.0
2021-08-04 19:38:30 +02:00
Eugene Pankov
621005eb27 build order fix 2021-08-04 19:28:41 +02:00
dependabot[bot]
d46e1de8aa Bump electron from 13.1.7 to 13.1.8
Bumps [electron](https://github.com/electron/electron) from 13.1.7 to 13.1.8.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v13.1.7...v13.1.8)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-04 04:03:36 +00:00
Eugene Pankov
c44f3c5f25 lint 2021-08-03 15:13:17 +02:00
dependabot[bot]
b3f9d48609 Bump webpack from 5.47.1 to 5.48.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.47.1 to 5.48.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.47.1...v5.48.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-03 04:06:40 +00:00
Eugene Pankov
edd7e9c7b7 set default cancel button in message boxes 2021-08-02 21:01:35 +02:00
Eugene Pankov
ab8061ab39 better new profile name handling - fixes #4325 2021-08-02 20:52:39 +02:00
Eugene Pankov
c1a1f53707 added a default name for duplicated profiles - fixes #4325 2021-08-02 20:03:02 +02:00
Eugene Pankov
04097a0ef5 serial: revert default CRLF processing to none - #4325 2021-08-02 20:00:38 +02:00
Eugene Pankov
85be974e64 reworked drop zones to allow more pane drop positions 2021-08-02 17:40:54 +02:00
Eugene Pankov
0df5fb4a34 fixed start page button flickering - fixes #4298 2021-08-02 09:25:05 +02:00
Eugene Pankov
920b2b85b3 Update appRoot.component.pug 2021-08-02 09:22:42 +02:00
Eugene Pankov
4e4788bf57 fixed glitchy window drag in macOS - fixes #4324 2021-08-02 09:17:08 +02:00
dependabot[bot]
9aa60a9d0d Bump typedoc from 0.21.4 to 0.21.5
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.21.4 to 0.21.5.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.21.4...v0.21.5)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:08:14 +00:00
dependabot[bot]
451ac51520 Bump electron-notarize from 1.0.0 to 1.0.1
Bumps [electron-notarize](https://github.com/electron/electron-notarize) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/electron/electron-notarize/releases)
- [Changelog](https://github.com/electron/electron-notarize/blob/master/.releaserc.json)
- [Commits](https://github.com/electron/electron-notarize/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: electron-notarize
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:07:48 +00:00
dependabot[bot]
04084aef33 Bump eslint from 7.31.0 to 7.32.0
Bumps [eslint](https://github.com/eslint/eslint) from 7.31.0 to 7.32.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.31.0...v7.32.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:07:29 +00:00
dependabot[bot]
4198ca3fae Bump ssh2 from 1.1.0 to 1.2.0
Bumps [ssh2](https://github.com/mscdex/ssh2) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/mscdex/ssh2/releases)
- [Commits](https://github.com/mscdex/ssh2/compare/v1.1.0...v1.2.0)

---
updated-dependencies:
- dependency-name: ssh2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:05:32 +00:00
Eugene Pankov
3b09dfa145 sftp: allow editing remote files locally - fixes #4311 2021-08-01 16:13:11 +02:00
Eugene Pankov
923b559857 bumped plugins 2021-08-01 10:17:53 +02:00
Eugene Pankov
58682b6bf1 serial: disable local echo by default 2021-08-01 10:17:52 +02:00
Eugene Pankov
88c4198145 Merge branch 'master' of github.com:Eugeny/tabby 2021-07-31 19:28:05 +02:00
Eugene Pankov
a6c535414f switched to webpack asset modules 2021-07-31 19:28:01 +02:00
Eugeny
6ebb7723ff Merge pull request #4212 from Eugeny/dependabot/npm_and_yarn/slugify-1.6.0
Bump slugify from 1.5.3 to 1.6.0
2021-07-31 19:17:14 +02:00
Eugeny
07dd6600dc Merge pull request #4300 from Eugeny/dependabot/npm_and_yarn/webpack-5.47.1
Bump webpack from 5.46.0 to 5.47.1
2021-07-31 19:12:05 +02:00
dependabot[bot]
3aaa419f8b Bump webpack from 5.46.0 to 5.47.1
Bumps [webpack](https://github.com/webpack/webpack) from 5.46.0 to 5.47.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.46.0...v5.47.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-30 04:05:41 +00:00
dependabot[bot]
8bfc1dc302 Bump slugify from 1.5.3 to 1.6.0
Bumps [slugify](https://github.com/simov/slugify) from 1.5.3 to 1.6.0.
- [Release notes](https://github.com/simov/slugify/releases)
- [Commits](https://github.com/simov/slugify/compare/v1.5.3...v1.6.0)

---
updated-dependencies:
- dependency-name: slugify
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-18 14:53:38 +00:00
123 changed files with 1780 additions and 5713 deletions

View File

@@ -433,6 +433,15 @@
"contributions": [
"code"
]
},
{
"login": "al-wi",
"name": "Alexander Wiedemann",
"avatar_url": "https://avatars.githubusercontent.com/u/11092199?v=4",
"profile": "https://github.com/al-wi",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -121,3 +121,8 @@ rules:
'@typescript-eslint/no-unsafe-argument': off
'@typescript-eslint/restrict-plus-operands': off
'@typescript-eslint/space-infix-ops': off
'@typescript-eslint/no-type-alias':
- error
- allowAliases: in-unions-and-intersections
allowLiterals: always
allowCallbacks: always

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v2.2.0
uses: actions/setup-node@v2.4.0
with:
node-version: 14
@@ -46,7 +46,7 @@ jobs:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v2.2.0
uses: actions/setup-node@v2.4.0
with:
node-version: 14
@@ -139,7 +139,7 @@ jobs:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v2.2.0
uses: actions/setup-node@v2.4.0
with:
node-version: 14
@@ -245,7 +245,7 @@ jobs:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v2.2.0
uses: actions/setup-node@v2.4.0
with:
node-version: 14

View File

@@ -12,7 +12,7 @@ jobs:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v2.2.0
uses: actions/setup-node@v2.4.0
with:
node-version: 14

View File

@@ -103,10 +103,10 @@ Tabby will run as a portable app on Windows, if you create a `data` folder in th
Plugins and themes can be installed directly from the Settings view inside Tabby.
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - makes paths and URLs in the terminal clickable
* [docker](https://github.com/Eugeny/tabby-docker) - connect to Docker containers
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
* [save-output](https://github.com/Eugeny/tabby-save-output) - record terminal output into a file
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to hterm tabs
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
<a name="themes"></a>
@@ -194,6 +194,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://git.io/JnP49"><img src="https://avatars.githubusercontent.com/u/63876444?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Logic Machine</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=logicmachine123" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/cypherbits"><img src="https://avatars.githubusercontent.com/u/10424900?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cypherbits</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=cypherbits" title="Documentation">📖</a></td>
<td align="center"><a href="https://modulolotus.net"><img src="https://avatars.githubusercontent.com/u/946421?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matthew Davidson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=KingMob" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
</tr>
</table>

View File

@@ -3,6 +3,7 @@ import * as promiseIpc from 'electron-promise-ipc'
import * as remote from '@electron/remote/main'
import * as path from 'path'
import * as fs from 'fs'
import { Subject, throttleTime } from 'rxjs'
import { loadConfig } from './config'
import { Window, WindowOptions } from './window'
@@ -19,6 +20,7 @@ export class Application {
private tray?: Tray
private ptyManager = new PTYManager()
private windows: Window[] = []
private globalHotkey$ = new Subject<void>()
userPluginsPath: string
constructor () {
@@ -33,12 +35,14 @@ export class Application {
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
globalShortcut.unregisterAll()
for (const spec of specs) {
globalShortcut.register(spec, () => {
this.onGlobalHotkey()
})
globalShortcut.register(spec, () => this.globalHotkey$.next())
}
})
this.globalHotkey$.pipe(throttleTime(100)).subscribe(() => {
this.onGlobalHotkey()
})
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
return pluginManager.install(this.userPluginsPath, name, version)
})

View File

@@ -27,7 +27,9 @@ app.on('activate', () => {
})
app.on('window-all-closed', () => {
app.quit()
if (process.platform !== 'darwin') {
app.quit()
}
})
process.on('uncaughtException' as any, err => {

View File

@@ -1,4 +1,4 @@
import * as nodePTY from 'node-pty'
import * as nodePTY from '@tabby-gang/node-pty'
import { StringDecoder } from './stringDecoder'
import { v4 as uuidv4 } from 'uuid'
import { ipcMain } from 'electron'

View File

@@ -1,7 +1,7 @@
import * as glasstron from 'glasstron'
import { Subject, Observable, debounceTime } from 'rxjs'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions } from 'electron'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage } from 'electron'
import ElectronConfig = require('electron-config')
import * as os from 'os'
import * as path from 'path'
@@ -28,6 +28,8 @@ abstract class GlasstronWindow extends BrowserWindow {
const macOSVibrancyType = process.platform === 'darwin' ? compareVersions.compare(macOSRelease().version, '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null
const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`)
export class Window {
ready: Promise<void>
private visible = new Subject<boolean>()
@@ -39,6 +41,7 @@ export class Window {
private lastVibrancy: { enabled: boolean, type?: string } | null = null
private disableVibrancyWhileDragging = false
private configStore: any
private touchBarControl: any
get visible$ (): Observable<boolean> { return this.visible }
get closed$ (): Observable<void> { return this.closed }
@@ -127,7 +130,15 @@ export class Window {
this.window.webContents.setVisualZoomLevelLimits(1, 1)
this.window.webContents.setZoomFactor(1)
if (process.platform !== 'darwin') {
if (process.platform === 'darwin') {
this.touchBarControl = new TouchBar.TouchBarSegmentedControl({
segments: [],
change: index => this.send('touchbar-selection', index),
})
this.window.setTouchBar(new TouchBar({
items: [this.touchBarControl],
}))
} else {
this.window.setMenu(null)
}
@@ -357,6 +368,14 @@ export class Window {
this.window.close()
})
ipcMain.on('window-set-touch-bar', (_event, segments, selectedIndex) => {
this.touchBarControl.segments = segments.map(s => ({
label: s.label,
icon: s.hasActivity ? activityIcon : undefined,
}))
this.touchBarControl.selectedIndex = selectedIndex
})
this.window.webContents.on('new-window', event => event.preventDefault())
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {

View File

@@ -14,7 +14,7 @@
"watch": "webpack --progress --color --watch"
},
"dependencies": {
"@angular/cdk": "^12.1.2",
"@angular/cdk": "^12.2.0",
"@electron/remote": "1.2.0",
"any-promise": "^1.3.0",
"electron-config": "2.0.0",
@@ -26,12 +26,12 @@
"keytar": "^7.7.0",
"mz": "^2.7.0",
"native-process-working-directory": "^1.0.2",
"node-pty": "^0.10.1",
"@tabby-gang/node-pty": "^0.11.0-beta.200",
"npm": "6",
"rxjs": "^7.2.0",
"source-map-support": "^0.5.19",
"v8-compile-cache": "^2.3.0",
"yargs": "^17.0.1"
"yargs": "^17.1.0"
},
"optionalDependencies": {
"macos-native-processlist": "^2.0.0",

View File

@@ -58,7 +58,7 @@ nodeModule.prototype.require = function (query: string) {
return originalModuleRequire.call(this, query)
}
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
export type ProgressCallback = (current: number, total: number) => void
export function initModuleLookup (userPluginsPath: string): void {
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))

View File

@@ -40,22 +40,8 @@ module.exports = {
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
test: /\.(png|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 999999,
},
},
},
{
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[ext]',
},
},
test: /\.(png|svg|ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
type: 'asset',
},
],
},

View File

@@ -44,7 +44,8 @@ module.exports = {
glasstron: 'commonjs glasstron',
mz: 'commonjs mz',
npm: 'commonjs npm',
'node-pty': 'commonjs node-pty',
'node:os': 'commonjs os',
'@tabby-gang/node-pty': 'commonjs @tabby-gang/node-pty',
path: 'commonjs path',
util: 'commonjs util',
'source-map-support': 'commonjs source-map-support',

View File

@@ -2,10 +2,10 @@
# yarn lockfile v1
"@angular/cdk@^12.1.2":
version "12.1.2"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.1.2.tgz#5c2407324d860737374d873bd4381bf7f90f8a61"
integrity sha512-ALupZejZDsVYcbNZcEH1cV8SDgVBL40FAwDnlSZxCgd0HOBHH0ZqQV+8z0uCQeMatoNM+SwmJ8Y1JXYh9Bqfiw==
"@angular/cdk@^12.2.0":
version "12.2.0"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.0.tgz#7c6de53522ef7cf911d86e187f3df2a90e8fee49"
integrity sha512-Dts+KIMz6EdzQxaWBFcNwgWAHVPkI5pnOGMidKKVOmjezSUN6mhfBKq8emgsddJMRAqz/1VHMAEaRkp0VoBKiA==
dependencies:
tslib "^2.2.0"
optionalDependencies:
@@ -96,6 +96,13 @@
dependencies:
debug "^4.3.1"
"@tabby-gang/node-pty@^0.11.0-beta.200":
version "0.11.0-beta.200"
resolved "https://registry.yarnpkg.com/@tabby-gang/node-pty/-/node-pty-0.11.0-beta.200.tgz#485cd6d85a04f4b272b81a9862578d7fc38cdfb5"
integrity sha512-32ANParjnd38SzvICaLYvEBlTZAE2sqsgEZPK6ITgd38FcCsS/yvvsDZcjkclbxApnMM2rJDaYjsZMa0lr9Iyg==
dependencies:
nan "^2.14.0"
"@types/mz@2.7.4":
version "2.7.4"
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
@@ -2095,13 +2102,6 @@ node-gyp@^5.0.2, node-gyp@^5.1.0:
tar "^4.4.12"
which "^1.3.1"
node-pty@^0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d"
integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg==
dependencies:
nan "^2.14.0"
noop-logger@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz"
@@ -3408,12 +3408,7 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
tslib@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
tslib@^2.2.0:
tslib@^2.0.0, tslib@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
@@ -3750,10 +3745,10 @@ yargs@^14.2.3:
y18n "^4.0.0"
yargs-parser "^15.0.1"
yargs@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
yargs@^17.1.0:
version "17.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf"
integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"

View File

@@ -7,7 +7,7 @@
"@angular/forms": "^12.0.0",
"@angular/platform-browser": "^12.0.0",
"@angular/platform-browser-dynamic": "^12.0.0",
"@fortawesome/fontawesome-free": "^5.15.3",
"@fortawesome/fontawesome-free": "^5.15.4",
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
"@sentry/cli": "^1.67.2",
"@sentry/electron": "^2.5.1",
@@ -19,7 +19,7 @@
"@types/node": "16.0.1",
"@types/sortablejs": "^1.10.7",
"@types/webpack-env": "^1.16.2",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.28.5",
"apply-loader": "2.0.0",
"axios": "^0.21.1",
@@ -27,21 +27,21 @@
"compare-versions": "^3.6.0",
"core-js": "^3.15.2",
"cross-env": "7.0.3",
"css-loader": "5.2.6",
"electron": "13.1.7",
"css-loader": "^6.2.0",
"electron": "13.1.9",
"electron-builder": "22.10.5",
"electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.5",
"eslint": "^7.31.0",
"electron-notarize": "^1.0.1",
"electron-rebuild": "^3.1.1",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.23.4",
"file-loader": "^6.2.0",
"graceful-fs": "^4.2.6",
"graceful-fs": "^4.2.8",
"html-loader": "2.1.2",
"json-loader": "0.5.7",
"lru-cache": "^6.0.0",
"macos-release": "^2.5.0",
"macos-release": "^3.0.0",
"ngx-sortablejs": "^11.1.0",
"ngx-toastr": "^14.0.0",
"node-abi": "^2.30.0",
@@ -58,21 +58,20 @@
"sass-loader": "^12.1.0",
"shell-quote": "^1.7.2",
"shelljs": "0.8.4",
"slugify": "^1.5.3",
"slugify": "^1.6.0",
"sortablejs": "^1.14.0",
"source-code-pro": "^2.38.0",
"source-map-loader": "^3.0.0",
"source-sans-pro": "3.6.0",
"ssh2": "^1.1.0",
"style-loader": "^3.1.0",
"ssh2": "^1.2.0",
"style-loader": "^3.2.1",
"svg-inline-loader": "^0.8.2",
"ts-loader": "^9.2.3",
"tslib": "^2.3.0",
"typedoc": "^0.21.4",
"typedoc": "^0.21.5",
"typescript": "^4.3.5",
"url-loader": "^4.1.1",
"val-loader": "4.0.0",
"webpack": "^5.46.0",
"webpack": "^5.50.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.0",
"yaml-loader": "0.6.0",
@@ -90,7 +89,7 @@
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
"start:prod": "electron app --debug",
"prod": "cross-env TABBY_DEV=1 electron app",
"docs": "typedoc --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
"docs": "typedoc --emit --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --emit --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --emit --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --emit --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
"lint": "eslint --ext ts */src */lib",
"postinstall": "node ./scripts/install-deps.js",
"patch": "patch-package; cd web; patch-package"

View File

@@ -1,15 +0,0 @@
diff --git a/node_modules/ssh2/lib/protocol/Protocol.js b/node_modules/ssh2/lib/protocol/Protocol.js
index b4d1ee0..1e3ac66 100644
--- a/node_modules/ssh2/lib/protocol/Protocol.js
+++ b/node_modules/ssh2/lib/protocol/Protocol.js
@@ -254,8 +254,8 @@ class Protocol {
);
if (greeting)
this._onWrite(greeting);
- this._onWrite(this._identRaw);
- this._onWrite(CRLF);
+ this._onWrite(Buffer.concat([this._identRaw, CRLF]));
+ // this._onWrite(CRLF);
});
}
_destruct(reason) {

View File

@@ -13,6 +13,10 @@ const configs = [
;(async () => {
for (const c of configs) {
log.info('build', c)
await promisify(webpack)(require(c))
const stats = await promisify(webpack)(require(c))
console.log(stats.toString({ colors: true }))
if (stats.hasErrors()) {
process.exit(1)
}
}
})()

View File

@@ -20,7 +20,7 @@ sh.cd('web')
sh.exec(`${npx} yarn install --force`)
sh.cd('..')
vars.builtinPlugins.forEach(plugin => {
vars.allPackages.forEach(plugin => {
log.info('deps', plugin)
sh.cd(plugin)
sh.exec(`${npx} yarn install --force`)

View File

@@ -9,7 +9,7 @@ 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/node-pty/build/Release/pty.node',
'app/node_modules/@tabby-gang/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',
]) {

View File

@@ -17,14 +17,14 @@ exports.builtinPlugins = [
'tabby-core',
'tabby-settings',
'tabby-terminal',
'tabby-electron',
'tabby-local',
'tabby-web',
'tabby-community-color-schemes',
'tabby-plugin-manager',
'tabby-ssh',
'tabby-serial',
'tabby-telnet',
'tabby-electron',
'tabby-local',
'tabby-plugin-manager',
]
exports.allPackages = [

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-community-color-schemes",
"version": "1.0.149",
"version": "1.0.150",
"description": "Community color schemes for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-core",
"version": "1.0.149",
"version": "1.0.150",
"description": "Tabby core",
"keywords": [
"tabby-builtin-plugin"
@@ -19,7 +19,6 @@
"devDependencies": {
"@types/js-yaml": "^4.0.0",
"bootstrap": "^4.1.3",
"core-js": "^3.1.2",
"deep-equal": "^2.0.5",
"deepmerge": "^4.1.1",
"electron-updater": "^4.0.6",

View File

@@ -25,7 +25,7 @@ export { DockingService, Screen } from '../services/docking.service'
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
export { HomeBaseService } from '../services/homeBase.service'
export { HotkeysService } from '../services/hotkeys.service'
export { KeyEventData, KeySequenceItem } from '../services/hotkeys.util'
export { KeyEventData, KeyName, Keystroke } from '../services/hotkeys.util'
export { NotificationsService } from '../services/notifications.service'
export { ThemesService } from '../services/themes.service'
export { ProfilesService } from '../services/profiles.service'

View File

@@ -13,6 +13,7 @@ export interface MessageBoxOptions {
detail?: string
buttons: string[]
defaultId?: number
cancelId?: number
}
export interface MessageBoxResult {

View File

@@ -46,6 +46,10 @@ export abstract class ProfileProvider<P extends Profile> {
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
getSuggestedName (profile: PartialProfile<P>): string|null {
return null
}
abstract getDescription (profile: PartialProfile<P>): string
quickConnect (query: string): PartialProfile<P>|null {

View File

@@ -4,5 +4,6 @@ export interface SelectorOption<T> {
result?: T
icon?: string
freeInputPattern?: string
color?: string
callback?: (string?) => void
}

View File

@@ -1,6 +1,6 @@
title-bar(
*ngIf='ready && !hostWindow.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullScreen'
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullscreen'
)
.content(
@@ -10,7 +10,7 @@ title-bar(
)
.tab-bar
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
&& !hostWindow.isFullScreen \
&& !hostWindow.isFullscreen \
&& config.store.appearance.frame == "thin" \
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
.tabs(
@@ -21,10 +21,6 @@ title-bar(
)
tab-header(
*ngFor='let tab of app.tabs; let idx = index',
cdkDrag,
[cdkDragData]='tab',
(cdkDragStarted)='onTabDragStart(tab)',
(cdkDragEnded)='onTabDragEnd()',
[index]='idx',
[tab]='tab',
[active]='tab == app.activeTab',

View File

@@ -182,17 +182,6 @@ export class AppRootComponent {
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
}
onTabDragStart (tab: BaseTabComponent) {
this.app.emitTabDragStarted(tab)
}
onTabDragEnd () {
setTimeout(() => {
this.app.emitTabDragEnded()
this.app.emitTabsChanged()
})
}
async generateButtonSubmenu (button: ToolbarButton) {
if (button.submenu) {
button.submenuItems = await button.submenu()

View File

@@ -7,7 +7,7 @@
(ngModelChange)='onFilterChange()'
)
.list-group(*ngIf='filteredOptions.length')
.list-group.list-group-light(*ngIf='filteredOptions.length')
a.list-group-item.list-group-item-action.d-flex.align-items-center(
#item,
(click)='selectOption(option)',
@@ -16,11 +16,13 @@
)
i.icon(
class='fa-fw {{option.icon}}',
style='color: {{option.color}}',
*ngIf='!iconIsSVG(option.icon)'
)
.icon(
[fastHtmlBind]='option.icon',
style='color: {{option.color}}',
*ngIf='iconIsSVG(option.icon)'
)
.mr-2.title {{getOptionText(option)}}
.text-muted {{option.description}}
.title.mr-2 {{getOptionText(option)}}
.description.no-wrap.text-muted {{option.description}}

View File

@@ -16,6 +16,11 @@
.title {
margin-left: 10px;
flex: none;
}
.description {
flex: 1 1 0;
}
input {

View File

@@ -50,8 +50,9 @@ export class SelectorModalComponent<T> {
if (!f) {
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
} else {
const terms = f.split(' ')
// 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 ?? terms.every(term => (x.name + (x.description ?? '')).toLowerCase().includes(term)))
}
this.selectedIndex = Math.max(0, this.selectedIndex)
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)

View File

@@ -6,8 +6,8 @@ import { TabsService, NewTabParameters } from '../services/tabs.service'
import { HotkeysService } from '../services/hotkeys.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
export type SplitOrientation = 'v' | 'h'
export type SplitDirection = 'r' | 't' | 'b' | 'l'
/**
* Describes a horizontal or vertical split row or column
@@ -93,13 +93,13 @@ export class SplitContainer {
return s
}
async serialize (): Promise<RecoveryToken> {
async serialize (tabsRecovery: TabRecoveryService): Promise<RecoveryToken> {
const children: any[] = []
for (const child of this.children) {
if (child instanceof SplitContainer) {
children.push(await child.serialize())
children.push(await child.serialize(tabsRecovery))
} else {
children.push(await child.getRecoveryToken())
children.push(await tabsRecovery.getFullRecoveryToken(child))
}
}
return {
@@ -126,10 +126,21 @@ export interface SplitSpannerInfo {
/**
* Represents a tab drop zone
*/
export interface SplitDropZoneInfo {
relativeToTab: BaseTabComponent
export type SplitDropZoneInfo = {
x: number
y: number
w: number
h: number
} & ({
type: 'absolute'
container: SplitContainer
position: number
} | {
type: 'relative'
relativeTo?: BaseTabComponent|SplitContainer
side: SplitDirection
}
})
/**
* Split tab is a tab that contains other tabs and allows further splitting them
@@ -370,7 +381,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/**
* Inserts a new `tab` to the `side` of the `relative` tab
*/
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|SplitContainer|null, side: SplitDirection): Promise<void> {
if (thing instanceof SplitTabComponent) {
const tab = thing
thing = tab.root
@@ -389,31 +400,40 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
thing.parent = this
}
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
let insertIndex = relative ? target.children.indexOf(relative) : -1
let target = relative ? this.getParentOf(relative) : null
if (!target) {
// Rewrap the root container just in case the orientation isn't compatibile
target = new SplitContainer()
target.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
target.children = [this.root]
target.ratios = [1]
this.root = target
}
let insertIndex = relative
? target.children.indexOf(relative) + ('tl'.includes(side) ? 0 : 1)
: 'tl'.includes(side) ? 0 : -1
if (
target.orientation === 'v' && ['l', 'r'].includes(side) ||
target.orientation === 'h' && ['t', 'b'].includes(side)
) {
// Inserting into a container but the orientation isn't compatible
const newContainer = new SplitContainer()
newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
newContainer.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
newContainer.children = relative ? [relative] : []
newContainer.ratios = [1]
target.children[insertIndex] = newContainer
target.children.splice(relative ? target.children.indexOf(relative) : -1, 1, newContainer)
target = newContainer
insertIndex = 0
}
if (insertIndex === -1) {
insertIndex = 0
} else {
insertIndex += side === 'l' || side === 't' ? 0 : 1
insertIndex = 'tl'.includes(side) ? 0 : 1
}
for (let i = 0; i < target.children.length; i++) {
target.ratios[i] *= target.children.length / (target.children.length + 1)
}
if (insertIndex === -1) {
insertIndex = target.ratios.length
}
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
target.children.splice(insertIndex, 0, thing)
@@ -550,7 +570,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
async getRecoveryToken (): Promise<any> {
return this.root.serialize()
return this.root.serialize(this.tabRecovery)
}
/** @hidden */
@@ -570,7 +590,11 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
return
}
this.add(tab, zone.relativeToTab, zone.side)
if (zone.type === 'relative') {
this.add(tab, zone.relativeTo ?? null, zone.side)
} else {
this.add(tab, zone.container.children[zone.position], zone.container.orientation === 'h' ? 'r' : 'b')
}
this.tabAdopted.next(tab)
}
@@ -622,6 +646,42 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
const size = root.orientation === 'v' ? h : w
const sizes = root.ratios.map(ratio => ratio * size)
const thickness = 10
if (root === this.root) {
this._dropZones.push({
x: x - thickness / 2,
y: y + thickness,
w: thickness,
h: h - thickness * 2,
type: 'relative',
side: 'l',
})
this._dropZones.push({
x,
y: y - thickness / 2,
w,
h: thickness,
type: 'relative',
side: 't',
})
this._dropZones.push({
x: x + w - thickness / 2,
y: y + thickness,
w: thickness,
h: h - thickness * 2,
type: 'relative',
side: 'r',
})
this._dropZones.push({
x,
y: y + h - thickness / 2,
w,
h: thickness,
type: 'relative',
side: 'b',
})
}
root.x = x
root.y = y
@@ -655,17 +715,65 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
element.style.width = '90%'
element.style.height = '90%'
}
for (const side of ['t', 'r', 'b', 'l']) {
this._dropZones.push({
relativeToTab: child,
side: side as SplitDirection,
})
}
}
}
offset += sizes[i]
if (i !== root.ratios.length - 1) {
// Spanner area
this._dropZones.push({
type: 'relative',
relativeTo: root.children[i],
side: root.orientation === 'v' ? 'b': 'r',
x: root.orientation === 'v' ? childX + thickness : childX + offset - thickness / 2,
y: root.orientation === 'v' ? childY + offset - thickness / 2 : childY + thickness,
w: root.orientation === 'v' ? childW - thickness * 2 : thickness,
h: root.orientation === 'v' ? thickness : childH - thickness * 2,
})
}
// Sides
if (root.orientation === 'v') {
this._dropZones.push({
x: childX,
y: childY + thickness,
w: thickness,
h: childH - thickness * 2,
type: 'relative',
relativeTo: child,
side: 'l',
})
this._dropZones.push({
x: childX + w - thickness,
y: childY + thickness,
w: thickness,
h: childH - thickness * 2,
type: 'relative',
relativeTo: child,
side: 'r',
})
} else {
this._dropZones.push({
x: childX + thickness,
y: childY,
w: childW - thickness * 2,
h: thickness,
type: 'relative',
relativeTo: child,
side: 't',
})
this._dropZones.push({
x: childX + thickness,
y: childY + childH - thickness,
w: childW - thickness * 2,
h: thickness,
type: 'relative',
relativeTo: child,
side: 'b',
})
}
if (i !== 0) {
this._spanners.push({
container: root,

View File

@@ -9,6 +9,7 @@
flex: 1 1 0;
width: 100%;
height: 100%;
opacity: 0;
background: rgba(255, 255, 255, .125);
border-radius: 5px;
@@ -21,6 +22,7 @@
border-radius: 3px;
> div {
opacity: 1;
background: rgba(255, 255, 255, .5);
}
}

View File

@@ -34,7 +34,7 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
) {
super(element)
this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
this.isActive = !!tab && tab !== this.parent && tab !== this.dropZone.relativeToTab
this.isActive = !!tab && tab !== this.parent && (this.dropZone.type === 'relative' || tab !== this.dropZone.container.children[this.dropZone.position])
this.layout()
})
}
@@ -44,26 +44,11 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
}
layout () {
const tabElement: HTMLElement|undefined = this.dropZone.relativeToTab.viewContainerEmbeddedRef?.rootNodes[0]
if (!tabElement) {
// being destroyed
return
}
const args = {
t: [0, 0, tabElement.clientWidth, tabElement.clientHeight / 5],
l: [0, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
r: [tabElement.clientWidth * 2 / 3, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
b: [0, tabElement.clientHeight * 4 / 5, tabElement.clientWidth, tabElement.clientHeight / 5],
}[this.dropZone.side]
this.setDimensions(
args[0] + tabElement.offsetLeft,
args[1] + tabElement.offsetTop,
args[2],
args[3],
'px'
this.dropZone.x,
this.dropZone.y,
this.dropZone.w,
this.dropZone.h,
)
}
}

View File

@@ -3,9 +3,9 @@ div
h1.tabby-title Tabby
sup α
.list-group
.list-group.list-group-light
a.list-group-item.list-group-item-action.d-flex(
*ngFor='let button of getButtons()',
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
(click)='button.click()',
)
.d-flex.align-self-center([innerHTML]='sanitizeIcon(button.icon)')
@@ -13,10 +13,10 @@ div
footer.d-flex.align-items-center
.btn-group.mr-auto
button.btn.btn-secondary((click)='homeBase.openGitHub()')
button.btn.btn-dark((click)='homeBase.openGitHub()')
i.fab.fa-github
span GitHub
button.btn.btn-secondary((click)='homeBase.reportBug()')
button.btn.btn-dark((click)='homeBase.reportBug()')
i.fas.fa-bug
span Report a problem

View File

@@ -32,4 +32,8 @@ export class StartPageComponent {
sanitizeIcon (icon?: string): any {
return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
}
buttonsTrackBy (btn: ToolbarButton): any {
return btn.title + btn.icon
}
}

View File

@@ -2,13 +2,17 @@
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
.activity-indicator(*ngIf='tab.activity$|async')
ng-container(*ngIf='!config.store.terminal.hideTabIndex')
.index(*ngIf='hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
.index(*ngIf='hostApp.platform !== Platform.macOS') {{index + 1}}
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform !== Platform.macOS') {{index + 1}}
.name(
[title]='tab.customTitle || tab.title',
[class.no-hover]='config.store.terminal.hideCloseButton'
cdkDrag,
cdkDragRootElement='tab-header',
[cdkDragData]='tab',
(cdkDragStarted)='onTabDragStart(tab)',
(cdkDragEnded)='onTabDragEnd()',
) {{tab.customTitle || tab.title}}
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') &times;

View File

@@ -60,6 +60,7 @@ export class TabHeaderComponent extends BaseComponent {
modal.result.then(result => {
this.tab.setTitle(result)
this.tab.customTitle = result
this.app.emitTabsChanged()
}).catch(() => null)
}
@@ -72,6 +73,17 @@ export class TabHeaderComponent extends BaseComponent {
return items.slice(1)
}
onTabDragStart (tab: BaseTabComponent) {
this.app.emitTabDragStarted(tab)
}
onTabDragEnd () {
setTimeout(() => {
this.app.emitTabDragEnded()
this.app.emitTabsChanged()
})
}
@HostBinding('class.flex-width') get isFlexWidthEnabled (): boolean {
return this.config.store.appearance.flexTabs
}

View File

@@ -22,6 +22,7 @@
min-width: 0;
margin-right: auto;
margin-bottom: 3px;
width: 100%;
label {
margin: 0;

View File

@@ -43,6 +43,7 @@ export class TransfersMenuComponent {
message: 'There are active file transfers',
buttons: ['Abort all', 'Do not abort'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'
import { ProfilesService } from './services/profiles.service'
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
import { PartialProfile, Profile } from './api'
/** @hidden */
@Injectable()
@@ -193,9 +194,13 @@ export class AppHotkeyProvider extends HotkeyProvider {
return [
...this.hotkeys,
...profiles.map(profile => ({
id: `profile.${profile.id}`,
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
name: `New tab: ${profile.name}`,
})),
]
}
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
return profile.id!.replace(/\./g, '-')
}
}

View File

@@ -151,8 +151,9 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
hotkeys.hotkey$.subscribe(async (hotkey) => {
if (hotkey.startsWith('profile.')) {
const id = hotkey.split('.')[1]
const profile = (await profilesService.getProfiles()).find(x => x.id === id)
const id = hotkey.substring(hotkey.indexOf('.') + 1)
const profiles = await profilesService.getProfiles()
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
if (profile) {
profilesService.openNewTabForProfile(profile)
}

View File

@@ -174,6 +174,9 @@ export class AppService {
* @param inputs Properties to be assigned on the new tab component instance
*/
openNewTab <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
if (params.type as any === SplitTabComponent) {
return this.openNewTabRaw(params)
}
const splitTab = this.tabsService.create({ type: SplitTabComponent })
const tab = this.tabsService.create(params)
splitTab.addTab(tab, null, 'r')

View File

@@ -375,6 +375,7 @@ export class ConfigService {
detail: e.toString(),
buttons: ['Erase config', 'Quit'],
defaultId: 1,
cancelId: 1,
})
if (result.response === 1) {
this.platform.quit()

View File

@@ -2,7 +2,7 @@ import { Injectable, Inject } from '@angular/core'
import * as mixpanel from 'mixpanel'
import { v4 as uuidv4 } from 'uuid'
import { ConfigService } from './config.service'
import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
import { PlatformService, BOOTSTRAP_DATA, BootstrapData, HostAppService } from '../api'
@Injectable({ providedIn: 'root' })
export class HomeBaseService {
@@ -13,6 +13,7 @@ export class HomeBaseService {
private constructor (
private config: ConfigService,
private platform: PlatformService,
private hostApp: HostAppService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) {
this.appVersion = platform.getAppVersion()
@@ -28,20 +29,11 @@ export class HomeBaseService {
reportBug (): void {
let body = `Version: ${this.appVersion}\n`
body += `Platform: ${process.platform} ${this.platform.getOSRelease()}\n`
const label = {
aix: 'OS: IBM AIX',
android: 'OS: Android',
darwin: 'OS: macOS',
freebsd: 'OS: FreeBSD',
linux: 'OS: Linux',
openbsd: 'OS: OpenBSD',
sunos: 'OS: Solaris',
win32: 'OS: Windows',
}[process.platform]
body += `Platform: ${this.hostApp.platform} ${process.arch} ${this.platform.getOSRelease()}\n`
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
body += `Plugins: ${plugins.join(', ') || 'none'}\n`
body += `Frontend: ${this.config.store.terminal?.frontend}\n\n`
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}`)
}
enableAnalytics (): void {

View File

@@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence, KeyEventData, KeySequenceItem } from './hotkeys.util'
import { KeyEventData, getKeyName, Keystroke, KeyName, getKeystrokeName, metaKeyName, altKeyName } from './hotkeys.util'
import { ConfigService } from './config.service'
import { HostAppService, Platform } from '../api/hostApp'
import { deprecate } from 'util'
@@ -12,14 +12,17 @@ export interface PartialHotkeyMatch {
matchedLength: number
}
const KEY_TIMEOUT = 2000
interface PastKeystroke {
keystroke: Keystroke
time: number
}
@Injectable({ providedIn: 'root' })
export class HotkeysService {
/** @hidden @deprecated */
key = new EventEmitter<KeyboardEvent>()
/** @hidden */
/** @hidden @deprecated */
matchedHotkey = new EventEmitter<string>()
/**
@@ -33,19 +36,35 @@ export class HotkeysService {
get hotkeyOff$ (): Observable<string> { return this._hotkeyOff }
/**
* Fired for each recognized hotkey
* Fired for each singular key
*/
get key$ (): Observable<KeyboardEvent> { return this._key }
get key$ (): Observable<KeyName> { return this._key }
/**
* Fired for each key event
*/
get keyEvent$ (): Observable<KeyboardEvent> { return this._keyEvent }
/**
* Fired for each singular key combination
*/
get keystroke$ (): Observable<Keystroke> { return this._keystroke }
private _hotkey = new Subject<string>()
private _hotkeyOff = new Subject<string>()
private _key = new Subject<KeyboardEvent>()
private currentEvents: KeyEventData[] = []
private _keyEvent = new Subject<KeyboardEvent>()
private _key = new Subject<KeyName>()
private _keystroke = new Subject<Keystroke>()
private disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = []
private pressedKeys = new Set<KeyName>()
private pressedKeyTimestamps = new Map<KeyName, number>()
private pressedHotkey: string|null = null
private lastMatchedHotkeyStartTime = performance.now()
private lastMatchedHotkeyEndTime = performance.now()
private pressedKeystroke: Keystroke|null = null
private lastKeystrokes: PastKeystroke[] = []
private shouldSaveNextKeystroke = true
private lastEventTimestamp = 0
private constructor (
private zone: NgZone,
@@ -53,164 +72,170 @@ export class HotkeysService {
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
hostApp: HostAppService,
) {
const events = ['keydown', 'keyup']
events.forEach(eventType => {
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
if (eventType === 'keyup' || document.querySelectorAll('input:focus').length === 0) {
this.pushKeystroke(eventType, nativeEvent)
if (hostApp.platform === Platform.Web) {
nativeEvent.preventDefault()
nativeEvent.stopPropagation()
}
}
})
})
this.config.ready$.toPromise().then(async () => {
const hotkeys = await this.getHotkeyDescriptions()
this.hotkeyDescriptions = hotkeys
const events = ['keydown', 'keyup']
events.forEach(eventType => {
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
this._keyEvent.next(nativeEvent)
this.pushKeyEvent(eventType, nativeEvent)
if (hostApp.platform === Platform.Web && this.matchActiveHotkey(true) !== null) {
nativeEvent.preventDefault()
nativeEvent.stopPropagation()
}
})
})
})
// deprecated
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
this.key$.subscribe(e => this.key.emit(e))
this.keyEvent$.subscribe(h => this.key.next(h))
this.key.subscribe = deprecate(s => this.keyEvent$.subscribe(s), 'key is deprecated, use keyEvent$')
}
/**
* Adds a new key event to the buffer
*
* @param name DOM event name
* @param eventName DOM event name
* @param nativeEvent event object
*/
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
nativeEvent['event'] = name
if (nativeEvent.timeStamp && this.currentEvents.find(x => x.time === nativeEvent.timeStamp)) {
pushKeyEvent (eventName: string, nativeEvent: KeyboardEvent): void {
if (nativeEvent.timeStamp === this.lastEventTimestamp) {
return
}
this.currentEvents.push({
nativeEvent['event'] = eventName
const eventData = {
ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey,
altKey: nativeEvent.altKey,
shiftKey: nativeEvent.shiftKey,
code: nativeEvent.code,
key: nativeEvent.key,
eventName: name,
eventName,
time: nativeEvent.timeStamp,
registrationTime: performance.now(),
})
this.processKeystrokes()
this.emitKeyEvent(nativeEvent)
}
/**
* Check the buffer for new complete keystrokes
*/
processKeystrokes (): void {
if (this.isEnabled()) {
this.zone.run(() => {
let fullMatches: {
id: string,
sequence: string[],
startTime: number,
endTime: number,
}[] = []
const currentSequence = this.getCurrentKeySequence()
const config = this.getHotkeysConfig()
for (const id in config) {
for (const sequence of config[id]) {
if (currentSequence.length < sequence.length) {
continue
}
if (sequence.every(
(x: string, index: number) =>
x.toLowerCase() ===
currentSequence[currentSequence.length - sequence.length + index].value.toLowerCase()
)) {
fullMatches.push({
id: id,
sequence,
startTime: currentSequence[currentSequence.length - sequence.length].firstEvent.registrationTime,
endTime: currentSequence[currentSequence.length - 1].lastEvent.registrationTime,
})
}
}
}
fullMatches.sort((a, b) => b.startTime - a.startTime + (b.sequence.length - a.sequence.length))
fullMatches = fullMatches.filter(x => x.startTime >= this.lastMatchedHotkeyStartTime)
fullMatches = fullMatches.filter(x => x.endTime > this.lastMatchedHotkeyEndTime)
const matched = fullMatches[0]?.id
if (matched) {
this.emitHotkeyOn(matched)
this.lastMatchedHotkeyStartTime = fullMatches[0].startTime
this.lastMatchedHotkeyEndTime = fullMatches[0].endTime
} else if (this.pressedHotkey) {
this.emitHotkeyOff(this.pressedHotkey)
}
})
}
}
private emitHotkeyOn (hotkey: string) {
if (this.pressedHotkey) {
this.emitHotkeyOff(this.pressedHotkey)
for (const [key, time] of this.pressedKeyTimestamps.entries()) {
if (time < performance.now() - 2000) {
this.removePressedKey(key)
}
}
console.debug('Matched hotkey', hotkey)
this._hotkey.next(hotkey)
this.pressedHotkey = hotkey
}
private emitHotkeyOff (hotkey: string) {
console.debug('Unmatched hotkey', hotkey)
this._hotkeyOff.next(hotkey)
this.pressedHotkey = null
}
const keyName = getKeyName(eventData)
if (eventName === 'keydown') {
this.addPressedKey(keyName, eventData)
this.shouldSaveNextKeystroke = true
this.updateModifiers(eventData)
}
if (eventName === 'keyup') {
const keystroke = getKeystrokeName([...this.pressedKeys])
if (this.shouldSaveNextKeystroke) {
this._keystroke.next(keystroke)
this.lastKeystrokes.push({
keystroke,
time: performance.now(),
})
this.shouldSaveNextKeystroke = false
}
this.removePressedKey(keyName)
}
emitKeyEvent (nativeEvent: KeyboardEvent): void {
if (this.pressedKeys.size) {
this.pressedKeystroke = getKeystrokeName([...this.pressedKeys])
} else {
this.pressedKeystroke = null
}
const matched = this.matchActiveHotkey()
this.zone.run(() => {
this._key.next(nativeEvent)
if (matched) {
this.emitHotkeyOn(matched)
} else if (this.pressedHotkey) {
this.emitHotkeyOff(this.pressedHotkey)
}
})
this.zone.run(() => {
this._key.next(getKeyName(eventData))
})
if (process.platform === 'darwin' && eventData.metaKey && eventName === 'keydown' && !['Ctrl', 'Shift', altKeyName, metaKeyName].includes(keyName)) {
// macOS will swallow non-modified keyups if Cmd is held down
this.pushKeyEvent('keyup', nativeEvent)
}
this.lastEventTimestamp = nativeEvent.timeStamp
}
clearCurrentKeystrokes (): void {
this.currentEvents = []
getCurrentKeystrokes (): Keystroke[] {
if (!this.pressedKeystroke) {
return []
}
return [...this.lastKeystrokes.map(x => x.keystroke), this.pressedKeystroke]
}
getCurrentKeySequence (): KeySequenceItem[] {
this.currentEvents = this.currentEvents.filter(x => performance.now() - x.time < KEY_TIMEOUT && x.registrationTime >= this.lastMatchedHotkeyStartTime)
return stringifyKeySequence(this.currentEvents)
}
matchActiveHotkey (partial = false): string|null {
if (!this.isEnabled() || !this.pressedKeystroke) {
return null
}
const matches: {
id: string,
sequence: string[],
}[] = []
getCurrentFullyMatchedHotkey (): string|null {
return this.pressedHotkey
}
const currentSequence = this.getCurrentKeystrokes()
getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] {
const currentStrokes = this.getCurrentKeySequence().map(x => x.value)
const config = this.getHotkeysConfig()
const result: PartialHotkeyMatch[] = []
for (const id in config) {
for (const sequence of config[id]) {
for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) {
if (sequence.slice(0, matchLength).every(
(x: string, index: number) =>
x.toLowerCase() ===
currentStrokes[currentStrokes.length - matchLength + index].toLowerCase()
)) {
result.push({
matchedLength: matchLength,
id,
strokes: sequence,
})
if (currentSequence.length < sequence.length) {
continue
}
if (sequence[sequence.length - 1] !== this.pressedKeystroke) {
continue
}
let lastIndex = 0
let matched = true
for (const item of sequence) {
const nextOffset = currentSequence.slice(lastIndex).findIndex(
x => x.toLowerCase() === item.toLowerCase()
)
if (nextOffset === -1) {
matched = false
break
}
lastIndex += nextOffset
}
if (partial ? lastIndex > 0 : matched) {
matches.push({
id,
sequence,
})
}
}
}
return result
matches.sort((a, b) => b.sequence.length - a.sequence.length)
if (!matches.length) {
return null
}
return matches[0].id
}
clearCurrentKeystrokes (): void {
this.lastKeystrokes = []
this.pressedKeys.clear()
this.pressedKeyTimestamps.clear()
this.pressedKeystroke = null
this.pressedHotkey = null
}
getHotkeyDescription (id: string): HotkeyDescription {
@@ -238,6 +263,42 @@ export class HotkeysService {
).reduce((a, b) => a.concat(b))
}
private updateModifiers (event: KeyEventData) {
for (const [prop, key] of Object.entries({
ctrlKey: 'Ctrl',
metaKey: metaKeyName,
altKey: altKeyName,
shiftKey: 'Shift',
})) {
if (!event[prop] && this.pressedKeys.has(key)) {
this.removePressedKey(key)
}
if (event[prop] && !this.pressedKeys.has(key)) {
this.addPressedKey(key, event)
}
}
}
private emitHotkeyOn (hotkey: string) {
if (this.pressedHotkey) {
if (this.pressedHotkey === hotkey) {
return
}
this.emitHotkeyOff(this.pressedHotkey)
}
if (document.querySelectorAll('input:focus').length === 0) {
console.debug('Matched hotkey', hotkey)
this._hotkey.next(hotkey)
this.pressedHotkey = hotkey
}
}
private emitHotkeyOff (hotkey: string) {
console.debug('Unmatched hotkey', hotkey)
this._hotkeyOff.next(hotkey)
this.pressedHotkey = null
}
private getHotkeysConfig () {
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
}
@@ -266,4 +327,14 @@ export class HotkeysService {
}
return keys
}
private addPressedKey (keyName: KeyName, eventData: KeyEventData) {
this.pressedKeys.add(keyName)
this.pressedKeyTimestamps.set(keyName, eventData.registrationTime)
}
private removePressedKey (key: KeyName) {
this.pressedKeys.delete(key)
this.pressedKeyTimestamps.delete(key)
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-type-alias */
export const metaKeyName = {
darwin: '⌘',
win32: 'Win',
@@ -24,88 +25,53 @@ export interface KeyEventData {
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export interface KeySequenceItem {
value: string
firstEvent: KeyEventData
lastEvent: KeyEventData
}
export type KeyName = string
export type Keystroke = string
export function stringifyKeySequence (events: KeyEventData[]): KeySequenceItem[] {
const items: KeySequenceItem[] = []
let pressedKeys: KeySequenceItem[] = []
events = events.slice()
const strictOrdering = ['Ctrl', metaKeyName, altKeyName, 'Shift']
function flushPressedKeys () {
if (pressedKeys.length) {
const v = {
firstEvent: pressedKeys[0].firstEvent,
lastEvent: pressedKeys[pressedKeys.length - 1].lastEvent,
}
pressedKeys = [
...strictOrdering.map(x => pressedKeys.find(p => p.value === x)).filter(x => !!x) as KeySequenceItem[],
...pressedKeys.filter(p => !strictOrdering.includes(p.value)),
]
items.push({
value: pressedKeys.map(x => x.value).join('-'),
...v,
})
pressedKeys = []
}
}
while (events.length > 0) {
const event = events.shift()!
// eslint-disable-next-line @typescript-eslint/init-declarations
let key: string
if (event.key === 'Control') {
key = 'Ctrl'
} else if (event.key === 'Meta') {
key = metaKeyName
} else if (event.key === 'Alt') {
key = altKeyName
} else if (event.key === 'Shift') {
key = 'Shift'
export function getKeyName (event: KeyEventData): KeyName {
// eslint-disable-next-line @typescript-eslint/init-declarations
let key: string
if (event.key === 'Control') {
key = 'Ctrl'
} else if (event.key === 'Meta') {
key = metaKeyName
} else if (event.key === 'Alt') {
key = altKeyName
} else if (event.key === 'Shift') {
key = 'Shift'
} else {
key = event.code
if (REGEX_LATIN_KEYNAME.test(event.key)) {
// Handle Dvorak etc via the reported "character" instead of the scancode
key = event.key.toUpperCase()
} else {
key = event.code
if (REGEX_LATIN_KEYNAME.test(event.key)) {
// Handle Dvorak etc via the reported "character" instead of the scancode
key = event.key.toUpperCase()
} else {
key = key.replace('Key', '')
key = key.replace('Arrow', '')
key = key.replace('Digit', '')
key = {
Comma: ',',
Period: '.',
Slash: '/',
Backslash: '\\',
IntlBackslash: '`',
Backquote: '~', // Electron says it's the tilde
Minus: '-',
Equal: '=',
Semicolon: ';',
Quote: '\'',
BracketLeft: '[',
BracketRight: ']',
}[key] ?? key
}
}
if (event.eventName === 'keydown') {
pressedKeys.push({
value: key,
firstEvent: event,
lastEvent: event,
})
}
if (event.eventName === 'keyup') {
flushPressedKeys()
key = key.replace('Key', '')
key = key.replace('Arrow', '')
key = key.replace('Digit', '')
key = {
Comma: ',',
Period: '.',
Slash: '/',
Backslash: '\\',
IntlBackslash: '`',
Backquote: '~', // Electron says it's the tilde
Minus: '-',
Equal: '=',
Semicolon: ';',
Quote: '\'',
BracketLeft: '[',
BracketRight: ']',
}[key] ?? key
}
}
flushPressedKeys()
return items
return key
}
export function getKeystrokeName (keys: KeyName[]): Keystroke {
const strictOrdering: KeyName[] = ['Ctrl', metaKeyName, altKeyName, 'Shift']
keys = [
...strictOrdering.map(x => keys.find(k => k === x)).filter(x => !!x) as KeyName[],
...keys.filter(k => !strictOrdering.includes(k)),
]
return keys.join('-')
}

View File

@@ -42,7 +42,7 @@ export class ProfilesService {
tab.setTitle(profile.name)
}
if (profile.disableDynamicTitle) {
tab['enableDynamicTitle'] = false
tab['disableDynamicTitle'] = true
}
return tab
}
@@ -84,8 +84,9 @@ export class ProfilesService {
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
const fullProfile = this.getConfigProxyForProfile(profile)
return {
icon: profile.icon,
name: profile.group ? `${fullProfile.group} / ${fullProfile.name}` : fullProfile.name,
icon: profile.icon,
color: profile.color,
description: this.providerForProfile(fullProfile)?.getDescription(fullProfile),
}
}
@@ -99,6 +100,7 @@ export class ProfilesService {
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
...this.selectorOptionForProfile(p),
icon: 'fas fa-history',
color: p.color,
callback: async () => {
if (p.id) {
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p

View File

@@ -34,9 +34,11 @@ export class TabRecoveryService {
const token = await tab.getRecoveryToken()
if (token) {
token.tabTitle = tab.title
token.tabCustomTitle = tab.customTitle
if (tab.color) {
token.tabColor = tab.color
}
token.disableDynamicTitle = tab['disableDynamicTitle']
}
return token
}
@@ -54,6 +56,8 @@ export class TabRecoveryService {
tab.inputs = tab.inputs ?? {}
tab.inputs.color = token.tabColor ?? null
tab.inputs.title = token.tabTitle || ''
tab.inputs.customTitle = token.tabCustomTitle || ''
tab.inputs.disableDynamicTitle = token.disableDynamicTitle
return tab
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)

View File

@@ -2,7 +2,6 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component'
import { TabRecoveryService } from './tabRecovery.service'
// eslint-disable-next-line @typescript-eslint/no-type-alias
export interface TabComponentType<T extends BaseTabComponent> {
// eslint-disable-next-line @typescript-eslint/prefer-function-type
new (...args: any[]): T

View File

@@ -13,6 +13,7 @@ import { TabsService } from './services/tabs.service'
import { HotkeysService } from './services/hotkeys.service'
import { PromptModalComponent } from './components/promptModal.component'
import { SplitLayoutProfilesService } from './profiles'
import { TAB_COLORS } from './utils'
/** @hidden */
@Injectable()
@@ -89,16 +90,6 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
}
}
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
/** @hidden */
@Injectable()
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
@@ -127,8 +118,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === tab.color)?.name,
submenu: COLORS.map(color => ({
sublabel: TAB_COLORS.find(x => x.value === tab.color)?.name,
submenu: TAB_COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: tab.color === color.value,

View File

@@ -230,26 +230,25 @@ hotkey-input-modal {
}
}
.list-group.list-group-flush .list-group-item:not(.list-group-item-action) {
.list-group.list-group-flush .list-group-item {
background: transparent;
border-color: rgba(0, 0, 0, 0.2);
&:not(:last-child) {
border-bottom: none;
}
&.list-group-item-action {
&:hover, &.active {
background: $list-group-hover-bg;
}
}
}
.list-group-light {
.list-group-item {
background: transparent;
border: none;
border-top: 1px solid rgba(0, 21, 43, .4);
&:first-child {
border-top: none;
}
&.list-group-item-action {
&:hover, &.active {
background: $list-group-hover-bg;

View File

@@ -26,8 +26,8 @@ $purple: #613d7c !default;
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
$content-bg-solid: #1D272D;
$table-bg: rgba(255,255,255,.05);
$table-bg-hover: rgba(255,255,255,.1);
$table-bg: rgba(255,255,255,.025);
$table-bg-hover: rgba(255,255,255,.05);
$table-border-color: rgba(255,255,255,.1);
$theme-colors: (
@@ -88,7 +88,7 @@ $list-group-item-padding-y: 0.8rem;
$list-group-item-padding-x: 1rem;
$list-group-hover-bg: $table-bg-hover;
$list-group-active-bg: rgba(255,255,255,.2);
$list-group-active-bg: rgba(255,255,255,.05);
$list-group-active-color: $component-active-color;
$list-group-active-border-color: translate;

View File

@@ -54,3 +54,13 @@ export class ResettableTimeout {
}
}
}
export const TAB_COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]

View File

@@ -50,11 +50,6 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
core-js@^3.1.2:
version "3.15.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.2.tgz#740660d2ff55ef34ce664d7e2455119c5bdd3d61"
integrity sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==
debug@4:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-electron",
"version": "1.0.149",
"version": "1.0.150",
"description": "Electron-specific bindings",
"keywords": [
"tabby-builtin-plugin"
@@ -20,7 +20,8 @@
"@angular/core": "^9.1.9"
},
"devDependencies": {
"winston": "^3.3.3",
"electron-promise-ipc": "^2.2.4"
"electron-promise-ipc": "^2.2.4",
"tmp-promise": "^3.0.2",
"winston": "^3.3.3"
}
}

View File

@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core'
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
import { TerminalColorSchemeProvider } from 'tabby-terminal'
import { SFTPContextMenuItemProvider } from 'tabby-ssh'
import { HyperColorSchemes } from './colorSchemes'
import { ElectronPlatformService } from './services/platform.service'
@@ -14,11 +15,12 @@ import { ElectronHostAppService } from './services/hostApp.service'
import { ElectronService } from './services/electron.service'
import { ElectronHotkeyProvider } from './hotkeys'
import { ElectronConfigProvider } from './config'
import { EditSFTPContextMenu } from './sftpContextMenu'
@NgModule({
providers: [
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: PlatformService, useClass: ElectronPlatformService },
{ provide: PlatformService, useExisting: ElectronPlatformService },
{ provide: HostWindowService, useExisting: ElectronHostWindow },
{ provide: HostAppService, useExisting: ElectronHostAppService },
{ provide: LogService, useClass: ElectronLogService },
@@ -27,6 +29,7 @@ import { ElectronConfigProvider } from './config'
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
{ provide: SFTPContextMenuItemProvider, useClass: EditSFTPContextMenu, multi: true },
],
})
export default class ElectronModule {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage, PowerSaveBlocker } from 'electron'
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, PowerSaveBlocker } from 'electron'
import * as remote from '@electron/remote'
export interface MessageBoxResponse {
@@ -15,7 +15,6 @@ export class ElectronService {
dialog: Dialog
clipboard: Clipboard
globalShortcut: GlobalShortcut
nativeImage: typeof NativeImage
screen: Screen
remote: Remote
process: any
@@ -38,7 +37,6 @@ export class ElectronService {
this.screen = remote.screen
this.dialog = remote.dialog
this.globalShortcut = remote.globalShortcut
this.nativeImage = remote.nativeImage
this.autoUpdater = remote.autoUpdater
this.powerSaveBlocker = remote.powerSaveBlocker
this.TouchBar = remote.TouchBar

View File

@@ -12,9 +12,9 @@ export interface Bounds {
@Injectable({ providedIn: 'root' })
export class ElectronHostWindow extends HostWindowService {
get isFullscreen (): boolean { return this._isFullScreen}
get isFullscreen (): boolean { return this._isFullscreen }
private _isFullScreen = false
private _isFullscreen = false
constructor (
zone: NgZone,
@@ -23,28 +23,26 @@ export class ElectronHostWindow extends HostWindowService {
) {
super()
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => {
this._isFullScreen = true
this._isFullscreen = true
}))
electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => {
this._isFullScreen = false
this._isFullscreen = false
}))
electron.ipcRenderer.on('host:window-shown', () => {
zone.run(() => this.windowShown.next())
})
electron.ipcRenderer.on('host:window-shown', () => zone.run(() => this.windowShown.next()))
electron.ipcRenderer.on('host:window-close-request', () => {
zone.run(() => this.windowCloseRequest.next())
})
electron.ipcRenderer.on('host:window-close-request', () => zone.run(() => {
this.windowCloseRequest.next()
}))
electron.ipcRenderer.on('host:window-moved', () => {
zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:window-moved', () => zone.run(() => {
this.windowMoved.next()
}))
electron.ipcRenderer.on('host:window-focused', () => {
zone.run(() => this.windowFocused.next())
})
electron.ipcRenderer.on('host:window-focused', () => zone.run(() => {
this.windowFocused.next()
}))
}
getWindow (): BrowserWindow {
@@ -64,7 +62,7 @@ export class ElectronHostWindow extends HostWindowService {
}
toggleFullscreen (): void {
this.getWindow().setFullScreen(!this._isFullScreen)
this.getWindow().setFullScreen(!this._isFullscreen)
}
minimize (): void {

View File

@@ -1,7 +1,9 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import * as gracefulFS from 'graceful-fs'
import * as fsSync from 'fs'
import * as os from 'os'
import { promisify } from 'util'
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core'
@@ -20,7 +22,7 @@ try {
var wnr = require('windows-native-registry')
} catch { }
@Injectable()
@Injectable({ providedIn: 'root' })
export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true
private configPath: string
@@ -118,7 +120,7 @@ export class ElectronPlatformService extends PlatformService {
async _saveConfigInternal (content: string): Promise<void> {
const tempPath = this.configPath + '.new'
await fs.writeFile(tempPath, content, 'utf8')
await fs.rename(tempPath, this.configPath)
await promisify(gracefulFS.rename)(tempPath, this.configPath)
}
getConfigPath (): string|null {
@@ -189,7 +191,7 @@ export class ElectronPlatformService extends PlatformService {
this.electron.app.exit(0)
}
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
async startUpload (options?: FileUploadOptions, paths?: string[]): Promise<FileUpload[]> {
options ??= { multiple: false }
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
@@ -197,18 +199,21 @@ export class ElectronPlatformService extends PlatformService {
properties.push('multiSelections')
}
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
},
)
if (result.canceled) {
return []
if (!paths) {
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
},
)
if (result.canceled) {
return []
}
paths = result.filePaths
}
return Promise.all(result.filePaths.map(async p => {
return Promise.all(paths.map(async p => {
const transfer = new ElectronFileUpload(p, this.electron)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
@@ -216,17 +221,20 @@ export class ElectronPlatformService extends PlatformService {
}))
}
async startDownload (name: string, mode: number, size: number): Promise<FileDownload|null> {
const result = await this.electron.dialog.showSaveDialog(
this.hostWindow.getWindow(),
{
defaultPath: name,
},
)
if (!result.filePath) {
return null
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {
if (!filePath) {
const result = await this.electron.dialog.showSaveDialog(
this.hostWindow.getWindow(),
{
defaultPath: name,
},
)
if (!result.filePath) {
return null
}
filePath = result.filePath
}
const transfer = new ElectronFileDownload(result.filePath, mode, size, this.electron)
const transfer = new ElectronFileDownload(filePath, mode, size, this.electron)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
return transfer

View File

@@ -1,56 +1,29 @@
import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
import { ipcRenderer } from 'electron'
import { Injectable, NgZone } from '@angular/core'
import { AppService, HostAppService, Platform } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
import { ElectronHostWindow } from './hostWindow.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl
private tabSegments: SegmentedControlSegment[] = []
private constructor (
private app: AppService,
private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
private zone: NgZone,
) {
if (this.hostApp.platform !== Platform.macOS) {
return
}
app.tabsChanged$.subscribe(() => this.updateTabs())
app.activeTabChange$.subscribe(() => this.updateTabs())
app.tabsChanged$.subscribe(() => this.update())
app.activeTabChange$.subscribe(() => this.update())
const activityIconPath = `${electron.app.getAppPath()}/assets/activity.png`
const activityIcon = this.electron.nativeImage.createFromPath(activityIconPath)
app.tabOpened$.subscribe(tab => {
tab.titleChange$.subscribe(title => {
const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) {
segment.label = this.shortenTitle(title)
this.tabsSegmentedControl.segments = this.tabSegments
}
})
tab.activity$.subscribe(hasActivity => {
const showIcon = this.app.activeTab !== tab && hasActivity
const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) {
segment.icon = showIcon ? activityIcon : undefined
}
})
tab.titleChange$.subscribe(() => this.update())
tab.activity$.subscribe(() => this.update())
})
}
updateTabs (): void {
this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title),
ipcRenderer.on('touchbar-selection', (_event, index) => this.zone.run(() => {
this.app.selectTab(this.app.tabs[index])
}))
this.tabsSegmentedControl.segments = this.tabSegments
this.tabsSegmentedControl.selectedIndex = this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : 0
}
update (): void {
@@ -58,20 +31,12 @@ export class TouchbarService {
return
}
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
segments: this.tabSegments,
selectedIndex: this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined,
change: (selectedIndex) => this.zone.run(() => {
this.app.selectTab(this.app.tabs[selectedIndex])
}),
})
const tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title),
hasActivity: this.app.activeTab !== tab && tab.hasActivity,
}))
const touchBar = new this.electron.TouchBar({
items: [
this.tabsSegmentedControl,
],
})
this.hostWindow.setTouchBar(touchBar)
ipcRenderer.send('window-set-touch-bar', tabSegments, this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined)
}
private shortenTitle (title: string): string {

View File

@@ -126,10 +126,11 @@ export class ElectronUpdaterService extends UpdaterService {
{
type: 'warning',
message: 'Installing the update will close all tabs and restart Tabby.',
buttons: ['Cancel', 'Update'],
defaultId: 1,
buttons: ['Update', 'Cancel'],
defaultId: 0,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
await this.downloaded
this.electron.autoUpdater.quitAndInstall()
}

View File

@@ -0,0 +1,59 @@
import * as tmp from 'tmp-promise'
import * as path from 'path'
import * as fs from 'fs'
import { Subject, debounceTime, debounce } from 'rxjs'
import { Injectable } from '@angular/core'
import { MenuItemOptions } from 'tabby-core'
import { SFTPFile, SFTPPanelComponent, SFTPContextMenuItemProvider, SFTPSession } from 'tabby-ssh'
import { ElectronPlatformService } from './services/platform.service'
/** @hidden */
@Injectable()
export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
weight = 0
constructor (
private platform: ElectronPlatformService,
) {
super()
}
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
if (item.isDirectory) {
return []
}
return [
{
click: () => this.edit(item, panel.sftp),
label: 'Edit locally',
},
]
}
private async edit (item: SFTPFile, sftp: SFTPSession) {
const tempDir = (await tmp.dir({ unsafeCleanup: true })).path
const tempPath = path.join(tempDir, item.name)
const transfer = await this.platform.startDownload(item.name, item.mode, item.size, tempPath)
if (!transfer) {
return
}
await sftp.download(item.fullPath, transfer)
this.platform.openPath(tempPath)
const events = new Subject<string>()
const watcher = fs.watch(tempPath, event => events.next(event))
events.pipe(debounceTime(1000), debounce(async event => {
if (event === 'rename') {
watcher.close()
}
const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
if (!upload.length) {
return
}
sftp.upload(item.fullPath, upload[0])
})).subscribe()
watcher.on('close', () => events.complete())
sftp.closed$.subscribe(() => watcher.close())
}
}

View File

@@ -16,6 +16,19 @@ async@^3.1.0:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -70,6 +83,11 @@ colorspace@1.1.x:
color "3.0.x"
text-hex "1.0.x"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -143,6 +161,11 @@ fn.name@1.x.x:
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -157,6 +180,18 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
glob@^7.1.3:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@@ -174,7 +209,15 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
inherits@^2.0.3, inherits@~2.0.3:
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -267,6 +310,13 @@ logform@^2.2.0:
ms "^2.1.1"
triple-beam "^1.3.0"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -302,6 +352,13 @@ object.entries@^1.1.3:
es-abstract "^1.18.0-next.1"
has "^1.0.3"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
one-time@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
@@ -309,6 +366,11 @@ one-time@^1.0.0:
dependencies:
fn.name "1.x.x"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -336,6 +398,13 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
rimraf@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -400,6 +469,20 @@ text-hex@1.0.x:
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
tmp-promise@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a"
integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==
dependencies:
tmp "^0.2.0"
tmp@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
dependencies:
rimraf "^3.0.0"
triple-beam@^1.2.0, triple-beam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
@@ -463,3 +546,8 @@ winston@^3.3.3:
stack-trace "0.0.x"
triple-beam "^1.3.0"
winston-transport "^4.4.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-local",
"version": "1.0.149",
"version": "1.0.150",
"description": "Tabby's local shell plugin",
"keywords": [
"tabby-builtin-plugin"
@@ -17,7 +17,6 @@
"author": "Eugene Pankov",
"license": "MIT",
"dependencies": {
"hterm-umdjs": "1.4.1",
"opentype.js": "^1.3.3"
},
"devDependencies": {

View File

@@ -106,10 +106,11 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `"${children[0].command}" is still running. Close?`,
buttons: ['Cancel', 'Kill'],
defaultId: 1,
buttons: ['Kill', 'Cancel'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
ngOnDestroy (): void {

View File

@@ -84,6 +84,10 @@ export class LocalProfilesService extends ProfileProvider<LocalProfile> {
}
}
getSuggestedName (profile: LocalProfile): string {
return this.getDescription(profile)
}
getDescription (profile: PartialProfile<LocalProfile>): string {
return profile.options?.command ?? ''
}

View File

@@ -174,11 +174,6 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hterm-umdjs@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957"
integrity sha512-r5JOmdDK1bZCmp3cKcuGRLVeum33H+pzD119ZxmQou+QUVe6SAVSz03HvKWVhM2Ao1Biv+fkhFDmnsaRPq0tFg==
is-arguments@^1.0.4, is-arguments@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-plugin-manager",
"version": "1.0.149",
"version": "1.0.150",
"description": "Tabby's plugin manager",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-serial",
"version": "1.0.149",
"version": "1.0.150",
"description": "Serial connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -25,10 +25,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
xon: false,
xoff: false,
xany: false,
inputMode: 'local-echo',
inputMode: null,
outputMode: null,
inputNewlines: null,
outputNewlines: 'crlf',
outputNewlines: null,
scripts: [],
},
}
@@ -92,6 +92,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
}
}
getSuggestedName (profile: SerialProfile): string {
return this.getDescription(profile)
}
getDescription (profile: SerialProfile): string {
return profile.options.port
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-settings",
"version": "1.0.149",
"version": "1.0.150",
"description": "Tabby terminal settings page",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -73,6 +73,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
message: 'Overwrite the config on the remote side and start syncing?',
buttons: ['Overwrite remote and sync', 'Cancel'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}
@@ -89,6 +90,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
message: 'Overwrite the local config and start syncing?',
buttons: ['Overwrite local and sync', 'Cancel'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}

View File

@@ -46,7 +46,10 @@
input.form-control.w-50(
type='text',
[(ngModel)]='profile.color',
placeholder='#000000'
placeholder='#000000',
alwaysVisibleTypeahead,
[ngbTypeahead]='colorsAutocomplete',
[resultFormatter]='colorsFormatter'
)
.form-line

View File

@@ -2,7 +2,7 @@
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService } from 'tabby-core'
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
const iconsData = require('../../../tabby-core/src/icons.json')
const iconsClassList = Object.keys(iconsData).map(
@@ -39,6 +39,20 @@ export class EditProfileModalComponent<P extends Profile> {
)].sort() as string[]
}
colorsAutocomplete = text$ => text$.pipe(
debounceTime(200),
distinctUntilChanged(),
map((q: string) =>
TAB_COLORS
.filter(x => !q || x.name.toLowerCase().startsWith(q.toLowerCase()))
.map(x => x.value)
)
)
colorsFormatter = value => {
return TAB_COLORS.find(x => x.value === value)?.name ?? value
}
ngOnInit () {
this._profile = this.profile
this.profile = this.profilesService.getConfigProxyForProfile(this.profile)

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { trigger, transition, style, animate } from '@angular/animations'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { HotkeysService, BaseComponent } from 'tabby-core'
import { HotkeysService, BaseComponent, Keystroke } from 'tabby-core'
const INPUT_TIMEOUT = 1000
@@ -36,7 +36,7 @@ const INPUT_TIMEOUT = 1000
],
})
export class HotkeyInputModalComponent extends BaseComponent {
@Input() value: string[] = []
@Input() value: Keystroke[] = []
@Input() timeoutProgress = 0
private lastKeyEvent: number|null = null
@@ -48,12 +48,14 @@ export class HotkeyInputModalComponent extends BaseComponent {
) {
super()
this.hotkeys.clearCurrentKeystrokes()
this.subscribeUntilDestroyed(hotkeys.key, (event) => {
this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeySequence().map(x => x.value)
this.subscribeUntilDestroyed(hotkeys.keyEvent$, event => {
event.preventDefault()
event.stopPropagation()
})
this.subscribeUntilDestroyed(hotkeys.keystroke$, keystroke => {
this.lastKeyEvent = performance.now()
this.value.push(keystroke)
})
}
splitKeys (keys: string): string[] {

View File

@@ -63,18 +63,31 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
})),
)
}
const profile = deepClone(base)
profile.id = null
profile.name = ''
const profile: PartialProfile<Profile> = deepClone(base)
delete profile.id
if (base.isTemplate) {
profile.name = ''
} else if (!base.isBuiltin) {
profile.name = `${base.name} copy`
}
profile.isBuiltin = false
profile.isTemplate = false
await this.editProfile(profile)
await this.showProfileEditModal(profile)
if (!profile.name) {
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? `${base.name} copy`
}
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
this.config.store.profiles = [profile, ...this.config.store.profiles]
await this.config.save()
}
async editProfile (profile: PartialProfile<Profile>): Promise<void> {
await this.showProfileEditModal(profile)
await this.config.save()
}
async showProfileEditModal (profile: PartialProfile<Profile>): Promise<void> {
const modal = this.ngbModal.open(
EditProfileModalComponent,
{ size: 'lg' },
@@ -89,8 +102,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
delete profile[k]
}
Object.assign(profile, result)
await this.config.save()
}
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
@@ -98,10 +109,11 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: `Delete "${profile.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
this.profilesService.providerForProfile(profile)?.deleteProfile(
this.profilesService.getConfigProxyForProfile(profile))
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
@@ -156,16 +168,18 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: `Delete "${group.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete the group's profiles?`,
buttons: ['Move to "Ungrouped"', 'Delete'],
defaultId: 0,
cancelId: 0,
}
)).response === 0) {
for (const profile of this.profiles.filter(x => x.group === group.name)) {

View File

@@ -42,10 +42,11 @@ export class VaultSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: 'Delete vault contents?',
buttons: ['Keep', 'Delete'],
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
await this.vault.setEnabled(false)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-ssh",
"version": "1.0.149",
"version": "1.0.150",
"description": "SSH connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,5 +1,5 @@
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
export const supportedAlgorithms: Record<string, string> = {}

View File

@@ -0,0 +1,12 @@
import { MenuItemOptions } from 'tabby-core'
import { SFTPFile } from '../session/sftp'
import { SFTPPanelComponent } from '../components/sftpPanel.component'
/**
* Extend to add items to the SFTPPanel context menu
*/
export abstract class SFTPContextMenuItemProvider {
weight = 0
abstract getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]>
}

View File

@@ -0,0 +1,2 @@
export * from './contextMenu'
export * from './interfaces'

View File

@@ -0,0 +1,53 @@
import { Profile } from 'tabby-core'
import { LoginScriptsOptions } from 'tabby-terminal'
export enum SSHAlgorithmType {
HMAC = 'hmac',
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey',
}
export interface SSHProfile extends Profile {
options: SSHProfileOptions
}
export interface SSHProfileOptions extends LoginScriptsOptions {
host: string
port?: number
user: string
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
password?: string
privateKeys?: string[]
keepaliveInterval?: number
keepaliveCountMax?: number
readyTimeout?: number
x11?: boolean
skipBanner?: boolean
jumpHost?: string
agentForward?: boolean
warnOnClose?: boolean
algorithms?: Record<string, string[]>
proxyCommand?: string
forwardedPorts?: ForwardedPortConfig[]
}
export enum PortForwardType {
Local = 'Local',
Remote = 'Remote',
Dynamic = 'Dynamic',
}
export interface ForwardedPortConfig {
type: PortForwardType
host: string
port: number
targetAddress: string
targetPort: number
}
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

@@ -16,7 +16,7 @@
div(*ngIf='!sftp') Connecting
div(*ngIf='sftp')
div(*ngIf='fileList === null') Loading
.list-group.list-group-light(*ngIf='fileList !== null')
.list-group.list-group-flush(*ngIf='fileList !== null')
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngIf='path !== "/"',
(click)='goUp()'

View File

@@ -1,18 +1,16 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { SSHSession } from '../api'
import { SFTPSession, SFTPFile } from '../session/sftp'
import { posix as path } from 'path'
import * as C from 'constants'
import { FileUpload, PlatformService } from 'tabby-core'
import { SFTPDeleteModalComponent } from './sftpDeleteModal.component'
import { posix as path } from 'path'
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
import { FileUpload, MenuItemOptions, PlatformService } from 'tabby-core'
import { SFTPSession, SFTPFile } from '../session/sftp'
import { SSHSession } from '../session/ssh'
import { SFTPContextMenuItemProvider } from '../api'
interface PathSegment {
name: string
path: string
}
/** @hidden */
@Component({
selector: 'sftp-panel',
template: require('./sftpPanel.component.pug'),
@@ -29,8 +27,10 @@ export class SFTPPanelComponent {
constructor (
private platform: PlatformService,
private ngbModal: NgbModal,
) { }
@Optional() @Inject(SFTPContextMenuItemProvider) protected contextMenuProviders: SFTPContextMenuItemProvider[],
) {
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
}
async ngOnInit (): Promise<void> {
this.sftp = await this.session.openSFTP()
@@ -96,28 +96,10 @@ export class SFTPPanelComponent {
}
async uploadOne (transfer: FileUpload): Promise<void> {
const itemPath = path.join(this.path, transfer.getName())
const tempPath = itemPath + '.tabby-upload'
this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
const savedPath = this.path
try {
const handle = await this.sftp.open(tempPath, 'w')
while (true) {
const chunk = await transfer.read()
if (!chunk.length) {
break
}
await handle.write(chunk)
}
handle.close()
await this.sftp.rename(tempPath, itemPath)
transfer.close()
if (this.path === savedPath) {
await this.navigate(this.path)
}
} catch (e) {
transfer.cancel()
this.sftp.unlink(tempPath)
throw e
if (this.path === savedPath) {
await this.navigate(this.path)
}
}
@@ -126,21 +108,7 @@ export class SFTPPanelComponent {
if (!transfer) {
return
}
try {
const handle = await this.sftp.open(itemPath, 'r')
while (true) {
const chunk = await handle.read()
if (!chunk.length) {
break
}
await transfer.write(chunk)
}
transfer.close()
handle.close()
} catch (e) {
transfer.cancel()
throw e
}
this.sftp.download(itemPath, transfer)
}
getModeString (item: SFTPFile): string {
@@ -159,31 +127,18 @@ export class SFTPPanelComponent {
return result
}
showContextMenu (item: SFTPFile, event: MouseEvent): void {
event.preventDefault()
this.platform.popupContextMenu([
{
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: `Delete ${item.fullPath}?`,
defaultId: 0,
buttons: ['Delete', 'Cancel'],
})).response === 0) {
await this.deleteItem(item)
this.navigate(this.path)
}
},
label: 'Delete',
},
], event)
async buildContextMenu (item: SFTPFile): Promise<MenuItemOptions[]> {
let items: MenuItemOptions[] = []
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(item, this)))) {
items.push({ type: 'separator' })
items = items.concat(section)
}
return items.slice(1)
}
async deleteItem (item: SFTPFile): Promise<void> {
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
modal.componentInstance.item = item
modal.componentInstance.sftp = this.sftp
await modal.result
async showContextMenu (item: SFTPFile, event: MouseEvent): Promise<void> {
event.preventDefault()
this.platform.popupContextMenu(await this.buildContextMenu(item), event)
}
close (): void {

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input } from '@angular/core'
import { ForwardedPort, ForwardedPortConfig, SSHSession } from '../api'
import { ForwardedPort } from '../session/forwards'
import { SSHSession } from '../session/ssh'
import { ForwardedPortConfig } from '../api'
/** @hidden */
@Component({

View File

@@ -2,7 +2,7 @@
import { Component, ViewChild } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent, PartialProfile } from 'tabby-core'
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
@@ -20,7 +20,7 @@ export class SSHProfileSettingsComponent {
supportedAlgorithms = supportedAlgorithms
algorithms: Record<string, Record<string, boolean>> = {}
jumpHosts: SSHProfile[]
jumpHosts: PartialProfile<SSHProfile>[]
@ViewChild('loginScriptsSettings') loginScriptsSettings: LoginScriptsSettingsComponent|null
constructor (

View File

@@ -2,11 +2,12 @@ import colors from 'ansi-colors'
import { Component, Injector, HostListener } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { Platform, RecoveryToken } from 'tabby-core'
import { PartialProfile, Platform, ProfilesService, RecoveryToken } from 'tabby-core'
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHProfile, SSHSession } from '../api'
import { SSHSession } from '../session/ssh'
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
import { SSHProfile } from '../api'
/** @hidden */
@@ -31,6 +32,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
injector: Injector,
public ssh: SSHService,
private ngbModal: NgbModal,
private profilesService: ProfilesService,
) {
super(injector)
}
@@ -77,13 +79,16 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
async setupOneSession (session: SSHSession): Promise<void> {
if (session.profile.options.jumpHost) {
const jumpConnection: SSHProfile|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
const jumpConnection: PartialProfile<SSHProfile>|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
if (!jumpConnection) {
throw new Error(`${session.profile.options.host}: jump host "${session.profile.options.jumpHost}" not found in your config`)
}
const jumpSession = new SSHSession(this.injector, jumpConnection)
const jumpSession = new SSHSession(
this.injector,
this.profilesService.getConfigProxyForProfile(jumpConnection)
)
await this.setupOneSession(jumpSession)
@@ -208,10 +213,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `Disconnect from ${this.profile?.options.host}?`,
buttons: ['Cancel', 'Disconnect'],
defaultId: 1,
buttons: ['Disconnect', 'Do not close'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
async openSFTP (): Promise<void> {

View File

@@ -22,6 +22,8 @@ import { RecoveryProvider } from './recoveryProvider'
import { SSHHotkeyProvider } from './hotkeys'
import { SFTPContextMenu } from './tabContextMenu'
import { SSHProfilesService } from './profiles'
import { SFTPContextMenuItemProvider } from './api/contextMenu'
import { CommonSFTPContextMenu } from './sftpContextMenu'
/** @hidden */
@NgModule({
@@ -41,6 +43,7 @@ import { SSHProfilesService } from './profiles'
{ provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: SFTPContextMenu, multi: true },
{ provide: ProfileProvider, useExisting: SSHProfilesService, multi: true },
{ provide: SFTPContextMenuItemProvider, useClass: CommonSFTPContextMenu, multi: true },
],
entryComponents: [
SSHProfileSettingsComponent,
@@ -105,3 +108,7 @@ export default class SSHModule {
await this.selector.show('Select an SSH profile', options)
}
}
export * from './api'
export { SFTPFile, SFTPSession } from './session/sftp'
export { SFTPPanelComponent, SFTPContextMenuItemProvider }

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core'
import { ProfileProvider, NewTabParameters, PartialProfile } from 'tabby-core'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
import { SSHTabComponent } from './components/sshTab.component'
import { PasswordStorageService } from './services/passwordStorage.service'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
@Injectable({ providedIn: 'root' })
@@ -81,6 +81,10 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
}
}
getSuggestedName (profile: SSHProfile): string {
return `${profile.options.user}@${profile.options.host}:${profile.options.port}`
}
getDescription (profile: PartialProfile<SSHProfile>): string {
return profile.options?.host ?? ''
}

View File

@@ -1,7 +1,7 @@
import * as keytar from 'keytar'
import { Injectable } from '@angular/core'
import { SSHProfile } from '../api'
import { VaultService } from 'tabby-core'
import { SSHProfile } from '../api'
export const VAULT_SECRET_TYPE_PASSWORD = 'ssh:password'
export const VAULT_SECRET_TYPE_PASSPHRASE = 'ssh:key-passphrase'

View File

@@ -4,11 +4,13 @@ import { Injectable, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Client } from 'ssh2'
import { exec } from 'child_process'
import { ChildProcess } from 'node:child_process'
import { Subject, Observable } from 'rxjs'
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService, PromptModalComponent } from 'tabby-core'
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHAlgorithmType, SSHProfile, SSHSession } from '../api'
import { SSHSession } from '../session/ssh'
import { ForwardedPort } from '../session/forwards'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
import { PasswordStorageService } from './passwordStorage.service'
import { ChildProcess } from 'node:child_process'
@Injectable({ providedIn: 'root' })
export class SSHService {

View File

@@ -0,0 +1,64 @@
import socksv5 from 'socksv5'
import { Server, Socket, createServer } from 'net'
import { ForwardedPortConfig, PortForwardType } from '../api'
export class ForwardedPort implements ForwardedPortConfig {
type: PortForwardType
host = '127.0.0.1'
port: number
targetAddress: string
targetPort: number
private listener: Server|null = null
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
if (this.type === PortForwardType.Local) {
const listener = this.listener = createServer(s => callback(
() => s,
() => s.destroy(),
s.remoteAddress ?? null,
s.remotePort ?? null,
this.targetAddress,
this.targetPort,
))
return new Promise((resolve, reject) => {
listener.listen(this.port, this.host)
listener.on('error', reject)
listener.on('listening', resolve)
})
} else if (this.type === PortForwardType.Dynamic) {
return new Promise((resolve, reject) => {
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
callback(
() => acceptConnection(true),
() => rejectConnection(),
null,
null,
info.dstAddr,
info.dstPort,
)
}) as Server
this.listener.on('error', reject)
this.listener.listen(this.port, this.host, resolve)
this.listener['useAuth'](socksv5.auth.None())
})
} else {
throw new Error('Invalid forward type for a local listener')
}
}
stopLocalListener (): void {
this.listener?.close()
}
toString (): string {
if (this.type === PortForwardType.Local) {
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
} if (this.type === PortForwardType.Remote) {
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
} else {
return `(dynamic) ${this.host}:${this.port}`
}
}
}

View File

@@ -1,8 +1,9 @@
import * as C from 'constants'
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
import { Subject, Observable } from 'rxjs'
import { posix as posixPath } from 'path'
import { NgZone } from '@angular/core'
import { wrapPromise } from 'tabby-core'
import { Injector, NgZone } from '@angular/core'
import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core'
import { SFTPWrapper } from 'ssh2'
import { promisify } from 'util'
@@ -70,9 +71,22 @@ export class SFTPFileHandle {
}
export class SFTPSession {
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
get closed$ (): Observable<void> { return this.closed }
private closed = new Subject<void>()
private zone: NgZone
private logger: Logger
constructor (private sftp: SFTPWrapper, injector: Injector) {
this.zone = injector.get(NgZone)
this.logger = injector.get(LogService).create('sftp')
sftp.on('close', () => {
this.closed.next()
this.closed.complete()
})
}
async readdir (p: string): Promise<SFTPFile[]> {
this.logger.debug('readdir', p)
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
return entries.map(entry => this._makeFile(
posixPath.join(p, entry.filename), entry,
@@ -80,10 +94,12 @@ export class SFTPSession {
}
readlink (p: string): Promise<string> {
this.logger.debug('readlink', p)
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
}
async stat (p: string): Promise<SFTPFile> {
this.logger.debug('stat', p)
const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
return {
name: posixPath.basename(p),
@@ -97,22 +113,68 @@ export class SFTPSession {
}
async open (p: string, mode: string): Promise<SFTPFileHandle> {
this.logger.debug('open', p)
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
return new SFTPFileHandle(this.sftp, handle, this.zone)
}
async rmdir (p: string): Promise<void> {
this.logger.debug('rmdir', p)
await promisify((f: any) => this.sftp.rmdir(p, f))()
}
async rename (oldPath: string, newPath: string): Promise<void> {
this.logger.debug('rename', oldPath, newPath)
await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
}
async unlink (p: string): Promise<void> {
this.logger.debug('unlink', p)
await promisify((f: any) => this.sftp.unlink(p, f))()
}
async upload (path: string, transfer: FileUpload): Promise<void> {
this.logger.info('Uploading into', path)
const tempPath = path + '.tabby-upload'
try {
const handle = await this.open(tempPath, 'w')
while (true) {
const chunk = await transfer.read()
if (!chunk.length) {
break
}
await handle.write(chunk)
}
handle.close()
await this.unlink(path)
await this.rename(tempPath, path)
transfer.close()
} catch (e) {
transfer.cancel()
this.unlink(tempPath)
throw e
}
}
async download (path: string, transfer: FileDownload): Promise<void> {
this.logger.info('Downloading', path)
try {
const handle = await this.open(path, 'r')
while (true) {
const chunk = await handle.read()
if (!chunk.length) {
break
}
await transfer.write(chunk)
}
transfer.close()
handle.close()
} catch (e) {
transfer.cancel()
throw e
}
}
private _makeFile (p: string, entry: FileEntry): SFTPFile {
return {
fullPath: p,

View File

@@ -5,126 +5,22 @@ import * as path from 'path'
import * as sshpk from 'sshpk'
import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi'
import socksv5 from 'socksv5'
import { Injector, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, Profile, LogService } from 'tabby-core'
import { BaseSession, LoginScriptsOptions } from 'tabby-terminal'
import { Server, Socket, createServer, createConnection } from 'net'
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService } from 'tabby-core'
import { BaseSession } from 'tabby-terminal'
import { Socket, createConnection } from 'net'
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
import { Subject, Observable } from 'rxjs'
import { ProxyCommandStream } from './services/ssh.service'
import { PasswordStorageService } from './services/passwordStorage.service'
import { ProxyCommandStream } from '../services/ssh.service'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { promisify } from 'util'
import { SFTPSession } from './session/sftp'
import { SFTPSession } from './sftp'
import { PortForwardType, SSHProfile } from '../api/interfaces'
import { ForwardedPort } from './forwards'
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
export enum SSHAlgorithmType {
HMAC = 'hmac',
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey',
}
export interface SSHProfile extends Profile {
options: SSHProfileOptions
}
export interface SSHProfileOptions extends LoginScriptsOptions {
host: string
port?: number
user: string
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
password?: string
privateKeys?: string[]
keepaliveInterval?: number
keepaliveCountMax?: number
readyTimeout?: number
x11?: boolean
skipBanner?: boolean
jumpHost?: string
agentForward?: boolean
warnOnClose?: boolean
algorithms?: Record<string, string[]>
proxyCommand?: string
forwardedPorts?: ForwardedPortConfig[]
}
export enum PortForwardType {
Local = 'Local',
Remote = 'Remote',
Dynamic = 'Dynamic',
}
export interface ForwardedPortConfig {
type: PortForwardType
host: string
port: number
targetAddress: string
targetPort: number
}
export class ForwardedPort implements ForwardedPortConfig {
type: PortForwardType
host = '127.0.0.1'
port: number
targetAddress: string
targetPort: number
private listener: Server|null = null
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
if (this.type === PortForwardType.Local) {
const listener = this.listener = createServer(s => callback(
() => s,
() => s.destroy(),
s.remoteAddress ?? null,
s.remotePort ?? null,
this.targetAddress,
this.targetPort,
))
return new Promise((resolve, reject) => {
listener.listen(this.port, this.host)
listener.on('error', reject)
listener.on('listening', resolve)
})
} else if (this.type === PortForwardType.Dynamic) {
return new Promise((resolve, reject) => {
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
callback(
() => acceptConnection(true),
() => rejectConnection(),
null,
null,
info.dstAddr,
info.dstPort,
)
}) as Server
this.listener.on('error', reject)
this.listener.listen(this.port, this.host, resolve)
this.listener['useAuth'](socksv5.auth.None())
})
} else {
throw new Error('Invalid forward type for a local listener')
}
}
stopLocalListener (): void {
this.listener?.close()
}
toString (): string {
if (this.type === PortForwardType.Local) {
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
} if (this.type === PortForwardType.Remote) {
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
} else {
return `(dynamic) ${this.host}:${this.port}`
}
}
}
interface AuthMethod {
type: 'none'|'publickey'|'agent'|'password'|'keyboard-interactive'|'hostbased'
name?: string
@@ -158,7 +54,7 @@ export class SSHSession extends BaseSession {
private config: ConfigService
constructor (
injector: Injector,
private injector: Injector,
public profile: SSHProfile,
) {
super(injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`))
@@ -241,7 +137,7 @@ export class SSHSession extends BaseSession {
if (!this.sftp) {
this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
}
return new SFTPSession(this.sftp, this.zone)
return new SFTPSession(this.sftp, this.injector)
}
async start (): Promise<void> {
@@ -613,9 +509,3 @@ export class SSHSession extends BaseSession {
}
}
}
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

@@ -0,0 +1,48 @@
import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { MenuItemOptions, PlatformService } from 'tabby-core'
import { SFTPSession, SFTPFile } from './session/sftp'
import { SFTPContextMenuItemProvider } from './api'
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
import { SFTPPanelComponent } from './components/sftpPanel.component'
/** @hidden */
@Injectable()
export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
weight = 10
constructor (
private platform: PlatformService,
private ngbModal: NgbModal,
) {
super()
}
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
return [
{
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: `Delete ${item.fullPath}?`,
defaultId: 0,
cancelId: 1,
buttons: ['Delete', 'Cancel'],
})).response === 0) {
await this.deleteItem(item, panel.sftp)
panel.navigate(panel.path)
}
},
label: 'Delete',
},
]
}
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
modal.componentInstance.item = item
modal.componentInstance.sftp = session
await modal.result
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-telnet",
"version": "1.0.149",
"version": "1.0.150",
"description": "Telnet/socket connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -119,9 +119,10 @@ export class TelnetTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `Disconnect from ${this.profile?.options.host}?`,
buttons: ['Cancel', 'Disconnect'],
defaultId: 1,
buttons: ['Disconnect', 'Do not close'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
}

View File

@@ -62,6 +62,10 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
}
}
getSuggestedName (profile: TelnetProfile): string|null {
return this.getDescription(profile) || null
}
getDescription (profile: TelnetProfile): string {
return profile.options.host ? `${profile.options.host}:${profile.options.port}` : ''
}

View File

@@ -170,11 +170,13 @@ export class TelnetSession extends BaseSession {
}
if (command === TelnetCommands.DO) {
if (option === TelnetOptions.NEGO_WINDOW_SIZE) {
this.resize(0, 0)
this.emitSize()
this.emitTelnet(TelnetCommands.WILL, option)
} else if (option === TelnetOptions.ECHO) {
this.echoEnabled = true
this.emitTelnet(TelnetCommands.WILL, option)
} else if (option === TelnetOptions.TERMINAL_TYPE) {
this.emitTelnet(TelnetCommands.WILL, option)
this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')]))
} else {
this.logger.debug('(!) Unhandled option')
@@ -210,10 +212,18 @@ export class TelnetSession extends BaseSession {
this.lastHeight = h
}
if (this.lastWidth && this.lastHeight && this.telnetProtocol) {
this.emitSize()
}
}
private emitSize () {
if (this.lastWidth && this.lastHeight) {
this.emitTelnetSuboption(TelnetOptions.NEGO_WINDOW_SIZE, Buffer.from([
this.lastWidth >> 8, this.lastWidth & 0xff,
this.lastHeight >> 8, this.lastHeight & 0xff,
]))
} else {
this.emitTelnet(TelnetCommands.WONT, TelnetOptions.NEGO_WINDOW_SIZE)
}
}

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