mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-02 06:31:52 +00:00
Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c5dbccf807 | ||
![]() |
ab4bf45c10 | ||
![]() |
61853428de | ||
![]() |
ae8c0128cb | ||
![]() |
744e731a22 | ||
![]() |
bb34e21791 | ||
![]() |
74f91b7cb3 | ||
![]() |
7bcf3dbabe | ||
![]() |
273111fb05 | ||
![]() |
8ba067d90e | ||
![]() |
b68f71ec62 | ||
![]() |
7100d12818 | ||
![]() |
f041f0f07a | ||
![]() |
6deb9ab48a | ||
![]() |
1e1c05c138 | ||
![]() |
8cfc20a81c | ||
![]() |
c853c96ae9 | ||
![]() |
85fe9eb4ec | ||
![]() |
cf5af26d6e | ||
![]() |
90e56e7605 | ||
![]() |
1c4e527db6 | ||
![]() |
75a0aadce4 | ||
![]() |
01e3e91e51 | ||
![]() |
7514fa41a1 | ||
![]() |
69115fb77a | ||
![]() |
99ab8dacd4 | ||
![]() |
e30d2cd85b | ||
![]() |
657915b1fe | ||
![]() |
90149def0a | ||
![]() |
1926eca929 | ||
![]() |
f20ba3e8bc | ||
![]() |
6f972ab4cc | ||
![]() |
129bc8a9f1 | ||
![]() |
4673aa498e | ||
![]() |
a2e0db2a16 | ||
![]() |
8def92eb5e | ||
![]() |
5b7e8f73b5 | ||
![]() |
7fa29b4b37 | ||
![]() |
a859baac97 | ||
![]() |
b7a676f668 | ||
![]() |
26d81f10a6 | ||
![]() |
be4cc804a2 | ||
![]() |
1b253ccb0a | ||
![]() |
ff49b9e38a | ||
![]() |
439e407595 | ||
![]() |
1eed32f8d8 | ||
![]() |
66098b5c6d | ||
![]() |
a725d25e46 | ||
![]() |
4e42dfd46b | ||
![]() |
c2657568a6 | ||
![]() |
dbe7b8cf56 | ||
![]() |
a82a65ed46 | ||
![]() |
893d9a9887 | ||
![]() |
1facd46901 | ||
![]() |
7af89e1d07 | ||
![]() |
50b2040d16 | ||
![]() |
a65505c498 | ||
![]() |
7e8c19e97b | ||
![]() |
8ac101cf9c | ||
![]() |
6297987e4f | ||
![]() |
8c8c49055b | ||
![]() |
cbd7c7c02f | ||
![]() |
57a198b082 | ||
![]() |
e245629c5a | ||
![]() |
760311ffa0 | ||
![]() |
2f13f3a401 | ||
![]() |
5ddf36d4c1 | ||
![]() |
a632a599d3 | ||
![]() |
ca9f11484c | ||
![]() |
9d224cbce2 | ||
![]() |
7df36b89c3 | ||
![]() |
8b62aa24ea | ||
![]() |
9502240480 | ||
![]() |
31efa2f9c1 | ||
![]() |
b40192f2ad | ||
![]() |
489ea5f891 | ||
![]() |
05eb24cd99 | ||
![]() |
5053743b1b | ||
![]() |
6d7f25870e | ||
![]() |
6cdee22164 | ||
![]() |
c043d5bc83 | ||
![]() |
d7741f07a1 | ||
![]() |
14c0b8891d | ||
![]() |
ea1d8e95f3 | ||
![]() |
50c20f08f8 | ||
![]() |
8f55333d23 | ||
![]() |
3be98e6244 | ||
![]() |
5e771534a8 | ||
![]() |
ba33f18af7 | ||
![]() |
82f3b61b5e | ||
![]() |
f587fd279c | ||
![]() |
8d13cb0fe8 | ||
![]() |
d1a6baf858 | ||
![]() |
25034342c3 | ||
![]() |
379775bcd3 | ||
![]() |
ff18926bf9 | ||
![]() |
37e564130e | ||
![]() |
d8a8d41614 | ||
![]() |
7db3335938 | ||
![]() |
c1051379c1 | ||
![]() |
f7a0fb488b | ||
![]() |
2706045cc2 | ||
![]() |
908f90cd52 | ||
![]() |
67bbbd7f65 | ||
![]() |
0008b2f022 | ||
![]() |
3e61630c6a | ||
![]() |
6f912dc12b | ||
![]() |
e1f2e176ce | ||
![]() |
f39b4c6dbe | ||
![]() |
c49ff68ed6 | ||
![]() |
891cf42338 | ||
![]() |
b9763044ee | ||
![]() |
46e0035327 | ||
![]() |
6df8707b6d | ||
![]() |
24b7922539 | ||
![]() |
485665d449 | ||
![]() |
e09a011c23 | ||
![]() |
833a348fdb | ||
![]() |
26ff6f17e7 | ||
![]() |
d026e634e5 | ||
![]() |
356a2f38b6 | ||
![]() |
bdb37a9a18 | ||
![]() |
22d89041f8 | ||
![]() |
d5285cf268 | ||
![]() |
3db98aa421 | ||
![]() |
47dba5b52c | ||
![]() |
72874a1e84 | ||
![]() |
fc1deb67e8 | ||
![]() |
d0bb3c731c | ||
![]() |
13e54a46d7 | ||
![]() |
55a975bc8b | ||
![]() |
a62752efec | ||
![]() |
4e97ce5117 | ||
![]() |
19a5f2dc2d | ||
![]() |
52433afd13 | ||
![]() |
e1d9f50426 | ||
![]() |
5837c61ac4 | ||
![]() |
8c03e5b1aa | ||
![]() |
66074e3eb6 | ||
![]() |
221746f3e7 | ||
![]() |
2c59e30c39 |
@@ -415,6 +415,24 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cypherbits",
|
||||
"name": "cypherbits",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10424900?v=4",
|
||||
"profile": "https://github.com/cypherbits",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "KingMob",
|
||||
"name": "Matthew Davidson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/946421?v=4",
|
||||
"profile": "https://modulolotus.net",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
19
.github/workflows/release.yml
vendored
Normal file
19
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: "tagged-release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: "Tagged Release"
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
draft: true
|
@@ -15,8 +15,8 @@ yarn
|
||||
```
|
||||
|
||||
```
|
||||
# Linux (Debian here as an example)
|
||||
sudo apt install libfontconfig-dev libsecret-1-dev bsdtar libnss3 libatk1.0-0 libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libgbm1
|
||||
# Linux (Debian/Ubuntu here as an example)
|
||||
sudo apt install libfontconfig-dev libsecret-1-dev libarchive-tools libnss3 libatk1.0-0 libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libgbm1 cmake
|
||||
yarn
|
||||
./scripts/build-native.js
|
||||
```
|
||||
|
11
README.md
11
README.md
@@ -7,12 +7,19 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ko-fi.com/J3J8KWTF">
|
||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg">
|
||||
<img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=2" width="150">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
### Downloads:
|
||||
|
||||
* [Latest release](https://github.com/Eugeny/tabby/releases/latest)
|
||||
* [Nightly build](https://nightly.link/Eugeny/tabby/workflows/build/master)
|
||||
|
||||
----
|
||||
|
||||
**Tabby** (formerly **Terminus**) is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
||||
|
||||
* Integrated SSH client and connection manager
|
||||
@@ -185,6 +192,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/ydcool"><img src="https://avatars.githubusercontent.com/u/5668295?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominic Yin</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ydcool" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bdr99"><img src="https://avatars.githubusercontent.com/u/2292715?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brandon Rothweiler</b></sub></a><br /><a href="#design-bdr99" title="Design">🎨</a></td>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'v8-compile-cache'
|
||||
import './portable'
|
||||
import 'source-map-support/register'
|
||||
import './sentry'
|
||||
|
@@ -117,7 +117,7 @@ export class Window {
|
||||
})
|
||||
|
||||
this.window.on('blur', () => {
|
||||
if (this.configStore.appearance?.dock !== 'off' && this.configStore.appearance?.dockHideOnBlur) {
|
||||
if ((this.configStore.appearance?.dock ?? 'off') !== 'off' && this.configStore.appearance?.dockHideOnBlur) {
|
||||
this.hide()
|
||||
}
|
||||
})
|
||||
|
@@ -14,6 +14,7 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^12.1.2",
|
||||
"@electron/remote": "1.2.0",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
@@ -29,18 +30,19 @@
|
||||
"npm": "6",
|
||||
"rxjs": "^7.2.0",
|
||||
"source-map-support": "^0.5.19",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"yargs": "^17.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"macos-native-processlist": "^2.0.0",
|
||||
"serialport": "^9.2.0",
|
||||
"windows-blurbehind": "^1.0.1",
|
||||
"windows-native-registry": "^3.0.0",
|
||||
"windows-native-registry": "^3.1.0",
|
||||
"windows-process-tree": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mz": "2.7.4",
|
||||
"@types/node": "16.0.0",
|
||||
"@types/node": "16.0.1",
|
||||
"ngx-filesize": "^2.0.16",
|
||||
"node-abi": "^2.30.0"
|
||||
},
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'v8-compile-cache'
|
||||
import '../lib/lru'
|
||||
import 'source-sans-pro/source-sans-pro.css'
|
||||
import 'source-code-pro/source-code-pro.css'
|
||||
|
@@ -28,7 +28,6 @@ body {
|
||||
|
||||
.form-line {
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.2);
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin: 0;
|
||||
@@ -115,7 +114,8 @@ ngb-typeahead-window {
|
||||
|
||||
.hover-reveal-parent:hover &,
|
||||
*:hover > &,
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -162,3 +162,27 @@ ngb-typeahead-window {
|
||||
.list-group-item > button {
|
||||
margin: -7px 0;
|
||||
}
|
||||
|
||||
|
||||
// Windows high contrast mode
|
||||
@media screen and (forced-colors: active) {
|
||||
.custom-switch .custom-control-label::before {
|
||||
background: buttonface;
|
||||
}
|
||||
|
||||
.custom-switch .custom-control-label::after {
|
||||
background: buttontext;
|
||||
}
|
||||
|
||||
.custom-switch .custom-control-input:checked ~ .custom-control-label::before {
|
||||
background: activetext;
|
||||
}
|
||||
|
||||
.custom-switch .custom-control-input:checked ~ .custom-control-label::after {
|
||||
background: canvas;
|
||||
}
|
||||
|
||||
color-scheme-preview, terminaltab > .content {
|
||||
forced-color-adjust: none;
|
||||
}
|
||||
}
|
||||
|
@@ -61,6 +61,7 @@ module.exports = {
|
||||
},
|
||||
externals: {
|
||||
'@electron/remote': 'commonjs @electron/remote',
|
||||
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||
child_process: 'commonjs child_process',
|
||||
electron: 'commonjs electron',
|
||||
fs: 'commonjs fs',
|
||||
|
@@ -34,6 +34,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||
'any-promise': 'commonjs any-promise',
|
||||
electron: 'commonjs electron',
|
||||
'electron-config': 'commonjs electron-config',
|
||||
|
@@ -2,6 +2,15 @@
|
||||
# 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==
|
||||
dependencies:
|
||||
tslib "^2.2.0"
|
||||
optionalDependencies:
|
||||
parse5 "^5.0.0"
|
||||
|
||||
"@electron/remote@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
||||
@@ -94,10 +103,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@16.0.0":
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.0.tgz#067a6c49dc7a5c2412a505628e26902ae967bf6f"
|
||||
integrity sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==
|
||||
"@types/node@*", "@types/node@16.0.1":
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.1.tgz#70cedfda26af7a2ca073fdcc9beb2fff4aa693f8"
|
||||
integrity sha512-hBOx4SUlEPKwRi6PrXuTGw1z6lz0fjsibcWCM378YxsSu/6+C30L6CR49zIBKHiwNWCYIcOLjg4OHKZaFeLAug==
|
||||
|
||||
JSONStream@^1.3.4, JSONStream@^1.3.5:
|
||||
version "1.3.5"
|
||||
@@ -2558,6 +2567,11 @@ parse-json@^2.2.0:
|
||||
dependencies:
|
||||
error-ex "^1.2.0"
|
||||
|
||||
parse5@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||
|
||||
path-exists@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
||||
@@ -3399,6 +3413,11 @@ tslib@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
|
||||
tslib@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
@@ -3519,6 +3538,11 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
v8-compile-cache@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||
|
||||
validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"
|
||||
@@ -3586,12 +3610,12 @@ windows-blurbehind@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.1.tgz#ff098713873304e38330b2c54cc41bb369b587b9"
|
||||
integrity sha512-1HzHfCiM1ayrbACJu5qE9zELV24uX/tINT6kxaZwLY3rtQAoeav6x9z7LFHWoLaGDN/sYbnK+9Vk0cz7fsk5HQ==
|
||||
|
||||
windows-native-registry@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/windows-native-registry/-/windows-native-registry-3.0.0.tgz#82e715df7a59d5054c768547d81e0bfc81a59d2e"
|
||||
integrity sha512-Mz/9a23UivwPc23DsTOL/ZCp/XXogT+6h/khk1psOfDDusXqpomBdxNdsBBE/BvIgOExjGom0XPOfEPiDnHy7A==
|
||||
windows-native-registry@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/windows-native-registry/-/windows-native-registry-3.1.0.tgz#909ef3254519fdec57d2f149ac59a2c9dc84419a"
|
||||
integrity sha512-WrDysn2V7dH+EYE6cS2RF+7r2P+M0pOYWtU8iBrjV2HaGkCLlUdGUWzOdzT0JPdWwz0BkVu3IOae2xmBajQqBA==
|
||||
dependencies:
|
||||
node-addon-api "^3.0.0"
|
||||
node-addon-api "^3.1.0"
|
||||
|
||||
windows-process-tree@^0.3.0:
|
||||
version "0.3.0"
|
||||
|
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -9,30 +9,32 @@
|
||||
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||
"@sentry/cli": "^1.64.2",
|
||||
"@sentry/electron": "^2.5.0",
|
||||
"@sentry/cli": "^1.67.2",
|
||||
"@sentry/electron": "^2.5.1",
|
||||
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
||||
"@types/electron-config": "^3.2.2",
|
||||
"@types/electron-debug": "^2.1.0",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/js-yaml": "^4.0.2",
|
||||
"@types/node": "16.0.0",
|
||||
"@types/node": "16.0.1",
|
||||
"@types/sortablejs": "^1.10.7",
|
||||
"@types/webpack-env": "^1.16.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"apply-loader": "2.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"clone-deep": "^4.0.1",
|
||||
"compare-versions": "^3.6.0",
|
||||
"core-js": "^3.15.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "5.2.6",
|
||||
"electron": "13.1.6",
|
||||
"electron": "13.1.7",
|
||||
"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.30.0",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"graceful-fs": "^4.2.6",
|
||||
@@ -40,10 +42,11 @@
|
||||
"json-loader": "0.5.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"macos-release": "^2.5.0",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-toastr": "^14.0.0",
|
||||
"node-abi": "^2.30.0",
|
||||
"node-sass": "^6.0.1",
|
||||
"npmlog": "4.1.2",
|
||||
"npmlog": "5.0.0",
|
||||
"npx": "^10.2.2",
|
||||
"patch-package": "^6.4.7",
|
||||
"pug": "^3.0.2",
|
||||
@@ -53,20 +56,23 @@
|
||||
"pug-static-loader": "2.0.0",
|
||||
"raw-loader": "4.0.2",
|
||||
"sass-loader": "^12.1.0",
|
||||
"shell-quote": "^1.7.2",
|
||||
"shelljs": "0.8.4",
|
||||
"slugify": "^1.5.3",
|
||||
"sortablejs": "^1.14.0",
|
||||
"source-code-pro": "^2.38.0",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-sans-pro": "3.6.0",
|
||||
"style-loader": "^3.0.0",
|
||||
"ssh2": "^1.1.0",
|
||||
"style-loader": "^3.1.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"ts-loader": "^9.2.3",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.21.2",
|
||||
"typedoc": "^0.21.4",
|
||||
"typescript": "^4.3.5",
|
||||
"url-loader": "^4.1.1",
|
||||
"val-loader": "4.0.0",
|
||||
"webpack": "^5.43.0",
|
||||
"webpack": "^5.46.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"yaml-loader": "0.6.0",
|
||||
|
@@ -1,13 +1,13 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/appInfo.js b/node_modules/app-builder-lib/out/appInfo.js
|
||||
index 25a159e..d8a0262 100644
|
||||
index 25a159e..bfe0590 100644
|
||||
--- a/node_modules/app-builder-lib/out/appInfo.js
|
||||
+++ b/node_modules/app-builder-lib/out/appInfo.js
|
||||
@@ -165,7 +165,7 @@ class AppInfo {
|
||||
get linuxPackageName() {
|
||||
const name = this.name; // https://github.com/electron-userland/electron-builder/issues/2963
|
||||
|
||||
|
||||
- return name.startsWith("@") ? this.sanitizedProductName : name;
|
||||
+ return 'tabby-terminal'
|
||||
+ return 'tabby-terminal';
|
||||
}
|
||||
|
||||
|
||||
get sanitizedName() {
|
||||
|
15
patches/ssh2+1.1.0.patch
Normal file
15
patches/ssh2+1.1.0.patch
Normal file
@@ -0,0 +1,15 @@
|
||||
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) {
|
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const sh = require('shelljs')
|
||||
const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
const webpack = require('webpack')
|
||||
@@ -8,8 +7,7 @@ const { promisify } = require('util')
|
||||
const configs = [
|
||||
'../app/webpack.main.config.js',
|
||||
'../app/webpack.config.js',
|
||||
'../web/webpack.config.js',
|
||||
...vars.builtinPlugins.map(x => `../${x}/webpack.config.js`),
|
||||
...vars.allPackages.map(x => `../${x}/webpack.config.js`),
|
||||
]
|
||||
|
||||
;(async () => {
|
||||
|
@@ -4,8 +4,11 @@ const path = require('path')
|
||||
const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
|
||||
const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
|
||||
const npx = `${localBinPath}/npx`;
|
||||
const localBinPath = path.resolve(__dirname, '../node_modules/.bin')
|
||||
const npx = `${localBinPath}/npx`
|
||||
|
||||
log.info('patch')
|
||||
sh.exec(`${npx} patch-package`)
|
||||
|
||||
log.info('deps', 'app')
|
||||
|
||||
@@ -18,16 +21,16 @@ sh.exec(`${npx} yarn install --force`)
|
||||
sh.cd('..')
|
||||
|
||||
vars.builtinPlugins.forEach(plugin => {
|
||||
log.info('deps', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec(`${npx} yarn install --force`)
|
||||
sh.cd('..')
|
||||
log.info('deps', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec(`${npx} yarn install --force`)
|
||||
sh.cd('..')
|
||||
})
|
||||
|
||||
if (['darwin', 'linux'].includes(process.platform)) {
|
||||
sh.cd('node_modules')
|
||||
for (let x of vars.builtinPlugins) {
|
||||
sh.ln('-fs', '../' + x, x)
|
||||
}
|
||||
sh.cd('..')
|
||||
sh.cd('node_modules')
|
||||
for (let x of vars.builtinPlugins) {
|
||||
sh.ln('-fs', '../' + x, x)
|
||||
}
|
||||
sh.cd('..')
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ const sh = require('shelljs')
|
||||
const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
|
||||
;[...vars.builtinPlugins, 'web'].forEach(plugin => {
|
||||
vars.allPackages.forEach(plugin => {
|
||||
log.info('bump', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec('npm --no-git-tag-version version ' + vars.version)
|
||||
|
@@ -10,7 +10,7 @@ exports.version = exports.version.substring(1).trim()
|
||||
exports.version = exports.version.replace('-', '-c')
|
||||
|
||||
if (exports.version.includes('-c')) {
|
||||
exports.version = semver.inc(exports.version, 'prepatch').replace('-0', '-nightly.0')
|
||||
exports.version = semver.inc(exports.version, 'prepatch').replace('-0', `-nightly.${process.env.REV ?? 0}`)
|
||||
}
|
||||
|
||||
exports.builtinPlugins = [
|
||||
@@ -26,6 +26,13 @@ exports.builtinPlugins = [
|
||||
'tabby-serial',
|
||||
'tabby-telnet',
|
||||
]
|
||||
|
||||
exports.allPackages = [
|
||||
...exports.builtinPlugins,
|
||||
'web',
|
||||
'tabby-web-demo',
|
||||
]
|
||||
|
||||
exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
|
1
tabby-community-color-schemes/.gitignore
vendored
1
tabby-community-color-schemes/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-community-color-schemes",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Community color schemes for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
1
tabby-core/.gitignore
vendored
1
tabby-core/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-core",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Tabby core",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -25,7 +25,6 @@
|
||||
"electron-updater": "^4.0.6",
|
||||
"js-yaml": "^4.0.0",
|
||||
"mixpanel": "^0.13.0",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-filesize": "^2.0.16",
|
||||
"ngx-perfect-scrollbar": "^10.1.0",
|
||||
"readable-stream": "3.6.0",
|
||||
|
@@ -16,11 +16,11 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||
export { HostWindowService } from './hostWindow'
|
||||
export { HostAppService, Platform } from './hostApp'
|
||||
export { FileProvider } from './fileProvider'
|
||||
export { ProfileProvider, Profile, ProfileSettingsComponent } from './profileProvider'
|
||||
export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
|
||||
export { PromptModalComponent } from '../components/promptModal.component'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
export { ConfigService, configMerge, ConfigProxy } from '../services/config.service'
|
||||
export { DockingService, Screen } from '../services/docking.service'
|
||||
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
|
||||
export { HomeBaseService } from '../services/homeBase.service'
|
||||
@@ -31,6 +31,6 @@ export { ProfilesService } from '../services/profiles.service'
|
||||
export { SelectorService } from '../services/selector.service'
|
||||
export { TabsService, NewTabParameters, TabComponentType } from '../services/tabs.service'
|
||||
export { UpdaterService } from '../services/updater.service'
|
||||
export { VaultService, Vault, VaultSecret, VAULT_SECRET_TYPE_FILE } from '../services/vault.service'
|
||||
export { VaultService, Vault, VaultSecret, VaultFileSecret, VAULT_SECRET_TYPE_FILE } from '../services/vault.service'
|
||||
export { FileProvidersService } from '../services/fileProviders.service'
|
||||
export * from '../utils'
|
||||
|
@@ -21,6 +21,7 @@ export interface MessageBoxResult {
|
||||
|
||||
export abstract class FileTransfer {
|
||||
abstract getName (): string
|
||||
abstract getMode (): number
|
||||
abstract getSize (): number
|
||||
abstract close (): void
|
||||
|
||||
@@ -95,7 +96,7 @@ export abstract class PlatformService {
|
||||
abstract loadConfig (): Promise<string>
|
||||
abstract saveConfig (content: string): Promise<void>
|
||||
|
||||
abstract startDownload (name: string, size: number): Promise<FileDownload|null>
|
||||
abstract startDownload (name: string, mode: number, size: number): Promise<FileDownload|null>
|
||||
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
|
||||
|
||||
startUploadFromDragEvent (event: DragEvent, multiple = false): FileUpload[] {
|
||||
@@ -188,6 +189,10 @@ export class HTMLFileUpload extends FileUpload {
|
||||
return this.file.name
|
||||
}
|
||||
|
||||
getMode (): number {
|
||||
return 0o644
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.file.size
|
||||
}
|
||||
|
@@ -1,44 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-type-alias */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { NewTabParameters } from '../services/tabs.service'
|
||||
|
||||
export interface Profile {
|
||||
id?: string
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
group?: string
|
||||
options?: Record<string, any>
|
||||
options: any
|
||||
|
||||
icon?: string
|
||||
color?: string
|
||||
disableDynamicTitle?: boolean
|
||||
disableDynamicTitle: boolean
|
||||
|
||||
weight?: number
|
||||
isBuiltin?: boolean
|
||||
isTemplate?: boolean
|
||||
weight: number
|
||||
isBuiltin: boolean
|
||||
isTemplate: boolean
|
||||
}
|
||||
|
||||
export interface ProfileSettingsComponent {
|
||||
profile: Profile
|
||||
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||
[K in keyof T]?: T[K]
|
||||
}, 'options'>, 'type'>, 'name'> & {
|
||||
type: string
|
||||
name: string
|
||||
options?: {
|
||||
[K in keyof T['options']]?: T['options'][K]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProfileSettingsComponent<P extends Profile> {
|
||||
profile: P
|
||||
save?: () => void
|
||||
}
|
||||
|
||||
export abstract class ProfileProvider {
|
||||
export abstract class ProfileProvider<P extends Profile> {
|
||||
id: string
|
||||
name: string
|
||||
supportsQuickConnect = false
|
||||
settingsComponent: new (...args: any[]) => ProfileSettingsComponent
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||
configDefaults = {}
|
||||
|
||||
abstract getBuiltinProfiles (): Promise<Profile[]>
|
||||
abstract getBuiltinProfiles (): Promise<PartialProfile<P>[]>
|
||||
|
||||
abstract getNewTabParameters (profile: Profile): Promise<NewTabParameters<BaseTabComponent>>
|
||||
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
|
||||
|
||||
abstract getDescription (profile: Profile): string
|
||||
abstract getDescription (profile: PartialProfile<P>): string
|
||||
|
||||
quickConnect (query: string): Profile|null {
|
||||
quickConnect (query: string): PartialProfile<P>|null {
|
||||
return null
|
||||
}
|
||||
|
||||
deleteProfile (profile: Profile): void { }
|
||||
deleteProfile (profile: P): void { }
|
||||
}
|
||||
|
@@ -2,26 +2,19 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
||||
import { SelectorService } from './services/selector.service'
|
||||
import { HostAppService, Platform } from './api/hostApp'
|
||||
import { Profile } from './api/profileProvider'
|
||||
import { PartialProfile, Profile } from './api/profileProvider'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { SelectorOption } from './api/selector'
|
||||
import { HotkeysService } from './services/hotkeys.service'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { AppService } from './services/app.service'
|
||||
import { NotificationsService } from './services/notifications.service'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private selector: SelectorService,
|
||||
private app: AppService,
|
||||
private hostApp: HostAppService,
|
||||
private profilesServices: ProfilesService,
|
||||
private profilesService: ProfilesService,
|
||||
private config: ConfigService,
|
||||
private notifications: NotificationsService,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
@@ -33,80 +26,22 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
}
|
||||
|
||||
async activate () {
|
||||
const recentProfiles: Profile[] = this.config.store.recentProfiles
|
||||
|
||||
const getProfileOptions = (profile): SelectorOption<void> => {
|
||||
const result: SelectorOption<void> = this.profilesServices.selectorOptionForProfile(profile)
|
||||
if (recentProfiles.includes(profile)) {
|
||||
result.icon = 'fas fa-history'
|
||||
}
|
||||
result.callback = () => this.launchProfile(profile)
|
||||
return result
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
if (profile) {
|
||||
this.launchProfile(profile)
|
||||
}
|
||||
|
||||
let options = recentProfiles.map(getProfileOptions)
|
||||
if (recentProfiles.length) {
|
||||
options.push({
|
||||
name: 'Clear recent connections',
|
||||
icon: 'fas fa-eraser',
|
||||
callback: () => {
|
||||
this.config.store.recentProfiles = []
|
||||
this.config.save()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let profiles = await this.profilesServices.getProfiles()
|
||||
|
||||
if (!this.config.store.terminal.showBuiltinProfiles) {
|
||||
profiles = profiles.filter(x => !x.isBuiltin)
|
||||
}
|
||||
|
||||
profiles = profiles.filter(x => !x.isTemplate)
|
||||
|
||||
options = [...options, ...profiles.map(getProfileOptions)]
|
||||
|
||||
try {
|
||||
const { SettingsTabComponent } = window['nodeRequire']('tabby-settings')
|
||||
options.push({
|
||||
name: 'Manage profiles',
|
||||
icon: 'fas fa-window-restore',
|
||||
callback: () => this.app.openNewTabRaw({
|
||||
type: SettingsTabComponent,
|
||||
inputs: { activeTab: 'profiles' },
|
||||
}),
|
||||
})
|
||||
} catch { }
|
||||
|
||||
if (this.profilesServices.getProviders().some(x => x.supportsQuickConnect)) {
|
||||
options.push({
|
||||
name: 'Quick connect',
|
||||
freeInputPattern: 'Connect to "%s"...',
|
||||
icon: 'fas fa-arrow-right',
|
||||
callback: query => this.quickConnect(query),
|
||||
})
|
||||
}
|
||||
await this.selector.show('Select profile', options)
|
||||
}
|
||||
|
||||
quickConnect (query: string) {
|
||||
for (const provider of this.profilesServices.getProviders()) {
|
||||
const profile = provider.quickConnect(query)
|
||||
if (profile) {
|
||||
this.launchProfile(profile)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.notifications.error(`Could not parse "${query}"`)
|
||||
}
|
||||
async launchProfile (profile: PartialProfile<Profile>) {
|
||||
await this.profilesService.openNewTabForProfile(profile)
|
||||
|
||||
async launchProfile (profile: Profile) {
|
||||
await this.profilesServices.openNewTabForProfile(profile)
|
||||
|
||||
const recentProfiles = this.config.store.recentProfiles
|
||||
recentProfiles.unshift(profile)
|
||||
if (recentProfiles.length > 5) {
|
||||
recentProfiles.pop()
|
||||
let recentProfiles = this.config.store.recentProfiles
|
||||
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||
recentProfiles.unshift(profile)
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
} else {
|
||||
recentProfiles = []
|
||||
}
|
||||
this.config.store.recentProfiles = recentProfiles
|
||||
this.config.save()
|
||||
|
@@ -14,15 +14,17 @@ title-bar(
|
||||
&& config.store.appearance.frame == "thin" \
|
||||
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||
.tabs(
|
||||
dnd-sortable-container,
|
||||
[sortableData]='app.tabs',
|
||||
cdkDropList,
|
||||
[cdkDropListOrientation]='(config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "bottom") ? "horizontal" : "vertical"',
|
||||
(cdkDropListDropped)='onTabsReordered($event)',
|
||||
cdkAutoDropGroup='app-tabs'
|
||||
)
|
||||
tab-header(
|
||||
*ngFor='let tab of app.tabs; let idx = index',
|
||||
dnd-sortable,
|
||||
[sortableIndex]='idx',
|
||||
(onDragStart)='onTabDragStart()',
|
||||
(onDragEnd)='onTabDragEnd()',
|
||||
cdkDrag,
|
||||
[cdkDragData]='tab',
|
||||
(cdkDragStarted)='onTabDragStart(tab)',
|
||||
(cdkDragEnded)='onTabDragEnd()',
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
@@ -30,7 +32,7 @@ title-bar(
|
||||
[@.disabled]='hasVerticalTabs()',
|
||||
(click)='app.selectTab(tab)',
|
||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
|
||||
)
|
||||
|
||||
.btn-group.background
|
||||
@@ -109,6 +111,7 @@ title-bar(
|
||||
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body.content-tab(
|
||||
#tabBodies,
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[class.content-tab-active]='tab == app.activeTab',
|
||||
[active]='tab == app.activeTab',
|
||||
|
@@ -132,6 +132,14 @@ $side-tab-width: 200px;
|
||||
window-controls {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cdk-drop-list-dragging tab-header:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
||||
import { Component, Inject, Input, HostListener, HostBinding, ViewChildren } from '@angular/core'
|
||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
|
||||
import { HostAppService, Platform } from '../api/hostApp'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
@@ -12,6 +13,7 @@ import { UpdaterService } from '../services/updater.service'
|
||||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { TabBodyComponent } from './tabBody.component'
|
||||
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@@ -57,14 +59,14 @@ export class AppRootComponent {
|
||||
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
||||
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
||||
@HostBinding('class.no-tabs') noTabs = true
|
||||
tabsDragging = false
|
||||
@ViewChildren(TabBodyComponent) tabBodies: TabBodyComponent[]
|
||||
unsortedTabs: BaseTabComponent[] = []
|
||||
updatesAvailable = false
|
||||
activeTransfers: FileTransfer[] = []
|
||||
activeTransfersDropdownOpen = false
|
||||
private logger: Logger
|
||||
|
||||
private constructor (
|
||||
constructor (
|
||||
private hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
public hostWindow: HostWindowService,
|
||||
@@ -126,11 +128,18 @@ export class AppRootComponent {
|
||||
this.app.tabOpened$.subscribe(tab => {
|
||||
this.unsortedTabs.push(tab)
|
||||
this.noTabs = false
|
||||
this.app.emitTabDragEnded()
|
||||
})
|
||||
|
||||
this.app.tabClosed$.subscribe(tab => {
|
||||
this.app.tabRemoved$.subscribe(tab => {
|
||||
for (const tabBody of this.tabBodies) {
|
||||
if (tabBody.tab === tab) {
|
||||
tabBody.detach()
|
||||
}
|
||||
}
|
||||
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
||||
this.noTabs = app.tabs.length === 0
|
||||
this.app.emitTabDragEnded()
|
||||
})
|
||||
|
||||
platform.fileTransferStarted$.subscribe(transfer => {
|
||||
@@ -173,13 +182,13 @@ export class AppRootComponent {
|
||||
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
|
||||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
onTabDragStart (tab: BaseTabComponent) {
|
||||
this.app.emitTabDragStarted(tab)
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.tabsDragging = false
|
||||
this.app.emitTabDragEnded()
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
@@ -194,6 +203,11 @@ export class AppRootComponent {
|
||||
return submenuItems.some(x => !!x.icon)
|
||||
}
|
||||
|
||||
onTabsReordered (event: CdkDragDrop<BaseTabComponent[]>) {
|
||||
moveItemInArray(this.app.tabs, event.previousIndex, event.currentIndex)
|
||||
this.app.emitTabsChanged()
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
||||
let buttons: ToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
||||
import { RecoveryToken } from '../api/tabRecovery'
|
||||
import { BaseComponent } from './base.component'
|
||||
|
||||
@@ -52,6 +52,10 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
* your tab state to be saved sooner
|
||||
*/
|
||||
protected recoveryStateChangedHint = new Subject<void>()
|
||||
protected viewContainer?: ViewContainerRef
|
||||
|
||||
/* @hidden */
|
||||
viewContainerEmbeddedRef?: EmbeddedViewRef<any>
|
||||
|
||||
private progressClearTimeout: number
|
||||
private titleChange = new Subject<string>()
|
||||
@@ -61,6 +65,8 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
private activity = new Subject<boolean>()
|
||||
private destroyed = new Subject<void>()
|
||||
|
||||
private _destroyCalled = false
|
||||
|
||||
get focused$ (): Observable<void> { return this.focused }
|
||||
get blurred$ (): Observable<void> { return this.blurred }
|
||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||
@@ -152,10 +158,29 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
this.blurred.next()
|
||||
}
|
||||
|
||||
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
|
||||
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
|
||||
this.viewContainer = container
|
||||
return this.viewContainerEmbeddedRef
|
||||
}
|
||||
|
||||
removeFromContainer (): void {
|
||||
if (!this.viewContainer || !this.viewContainerEmbeddedRef) {
|
||||
return
|
||||
}
|
||||
this.viewContainer.detach(this.viewContainer.indexOf(this.viewContainerEmbeddedRef))
|
||||
this.viewContainerEmbeddedRef = undefined
|
||||
this.viewContainer = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the tab is closed
|
||||
*/
|
||||
destroy (skipDestroyedEvent = false): void {
|
||||
if (this._destroyCalled) {
|
||||
return
|
||||
}
|
||||
this._destroyCalled = true
|
||||
this.focused.complete()
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
@@ -166,6 +191,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
this.destroyed.next()
|
||||
}
|
||||
this.destroyed.complete()
|
||||
this.hostView.destroy()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
|
@@ -1,19 +1,19 @@
|
||||
.modal-body
|
||||
input.form-control(
|
||||
[type]='password ? "password" : "text"',
|
||||
[type]='password ? "password" : "text"',
|
||||
autofocus,
|
||||
[(ngModel)]='value',
|
||||
#input,
|
||||
[placeholder]='prompt',
|
||||
[(ngModel)]='value',
|
||||
#input,
|
||||
[placeholder]='prompt',
|
||||
(keyup.enter)='ok()',
|
||||
(keyup.esc)='cancel()',
|
||||
)
|
||||
.d-flex.align-items-start.mt-2
|
||||
checkbox(
|
||||
*ngIf='showRememberCheckbox',
|
||||
[(ngModel)]='remember',
|
||||
[(ngModel)]='remember',
|
||||
text='Remember'
|
||||
)
|
||||
button.btn.btn-primary.ml-auto(
|
||||
(click)='ok()',
|
||||
) Enter
|
||||
) OK
|
||||
|
@@ -2,5 +2,5 @@
|
||||
input.form-control(type='text', #input, [(ngModel)]='value', (keyup.enter)='save()', autofocus)
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-secondary((click)='close()') Cancel
|
||||
button.btn.btn-primary((click)='save()') Save
|
||||
button.btn.btn-secondary((click)='close()') Cancel
|
||||
|
@@ -4,4 +4,4 @@
|
||||
pre {{error}}
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='close()') Close
|
||||
button.btn.btn-primary((click)='close()') Close
|
||||
|
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { HostBinding, ElementRef } from '@angular/core'
|
||||
import { BaseComponent } from './base.component'
|
||||
|
||||
export abstract class SelfPositioningComponent extends BaseComponent {
|
||||
@HostBinding('style.left') cssLeft: string
|
||||
@HostBinding('style.top') cssTop: string
|
||||
@HostBinding('style.width') cssWidth: string | null
|
||||
@HostBinding('style.height') cssHeight: string | null
|
||||
|
||||
constructor (protected element: ElementRef) { super() }
|
||||
|
||||
protected setDimensions (x: number, y: number, w: number, h: number, unit = '%'): void {
|
||||
this.cssLeft = `${x}${unit}`
|
||||
this.cssTop = `${y}${unit}`
|
||||
this.cssWidth = w ? `${w}${unit}` : null
|
||||
this.cssHeight = h ? `${h}${unit}` : null
|
||||
}
|
||||
}
|
@@ -123,6 +123,14 @@ export interface SplitSpannerInfo {
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tab drop zone
|
||||
*/
|
||||
export interface SplitDropZoneInfo {
|
||||
relativeToTab: BaseTabComponent
|
||||
side: SplitDirection
|
||||
}
|
||||
|
||||
/**
|
||||
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||
* You'll mainly encounter it inside [[AppService]].tabs
|
||||
@@ -137,6 +145,13 @@ export interface SplitSpannerInfo {
|
||||
[index]='spanner.index'
|
||||
(change)='onSpannerAdjusted(spanner)'
|
||||
></split-tab-spanner>
|
||||
<split-tab-drop-zone
|
||||
*ngFor='let dropZone of _dropZones'
|
||||
[parent]='this'
|
||||
[dropZone]='dropZone'
|
||||
(tabDropped)='onTabDropped($event, dropZone)'
|
||||
>
|
||||
</split-tab-drop-zone>
|
||||
`,
|
||||
styles: [require('./splitTab.component.scss')],
|
||||
})
|
||||
@@ -157,6 +172,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
/** @hidden */
|
||||
_spanners: SplitSpannerInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_dropZones: SplitDropZoneInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_allFocusMode = false
|
||||
|
||||
@@ -166,12 +184,19 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||
|
||||
private tabAdded = new Subject<BaseTabComponent>()
|
||||
private tabAdopted = new Subject<BaseTabComponent>()
|
||||
private tabRemoved = new Subject<BaseTabComponent>()
|
||||
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||
private focusChanged = new Subject<BaseTabComponent>()
|
||||
private initialized = new Subject<void>()
|
||||
|
||||
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
||||
|
||||
/**
|
||||
* Fired when an existing top-level tab is dragged into this tab
|
||||
*/
|
||||
get tabAdopted$ (): Observable<BaseTabComponent> { return this.tabAdopted }
|
||||
|
||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||
|
||||
/**
|
||||
@@ -330,11 +355,27 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
}
|
||||
|
||||
addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||
return this.add(tab, relative, side)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new `tab` to the `side` of the `relative` tab
|
||||
*/
|
||||
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||
tab.parent = this
|
||||
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||
if (thing instanceof SplitTabComponent) {
|
||||
const tab = thing
|
||||
thing = tab.root
|
||||
tab.root = new SplitContainer()
|
||||
for (const child of thing.getAllTabs()) {
|
||||
child.removeFromContainer()
|
||||
}
|
||||
tab.destroy()
|
||||
}
|
||||
|
||||
if (thing instanceof BaseTabComponent) {
|
||||
thing.parent = this
|
||||
}
|
||||
|
||||
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
|
||||
let insertIndex = relative ? target.children.indexOf(relative) : -1
|
||||
@@ -362,19 +403,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||
}
|
||||
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
||||
target.children.splice(insertIndex, 0, tab)
|
||||
target.children.splice(insertIndex, 0, thing)
|
||||
|
||||
this.recoveryStateChangedHint.next()
|
||||
|
||||
await this.initialized$.toPromise()
|
||||
|
||||
this.attachTabView(tab)
|
||||
|
||||
setImmediate(() => {
|
||||
this.layout()
|
||||
this.tabAdded.next(tab)
|
||||
this.focus(tab)
|
||||
})
|
||||
for (const tab of thing instanceof SplitContainer ? thing.getAllTabs() : [thing]) {
|
||||
this.attachTabView(tab)
|
||||
this.onAfterTabAdded(tab)
|
||||
}
|
||||
}
|
||||
|
||||
removeTab (tab: BaseTabComponent): void {
|
||||
@@ -386,8 +424,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
parent.ratios.splice(index, 1)
|
||||
parent.children.splice(index, 1)
|
||||
|
||||
this.detachTabView(tab)
|
||||
tab.removeFromContainer()
|
||||
tab.parent = null
|
||||
this.viewRefs.delete(tab)
|
||||
|
||||
this.layout()
|
||||
|
||||
@@ -399,6 +438,21 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
}
|
||||
|
||||
replaceTab (tab: BaseTabComponent, newTab: BaseTabComponent): void {
|
||||
const parent = this.getParentOf(tab)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
const position = parent.children.indexOf(tab)
|
||||
parent.children[position] = newTab
|
||||
tab.removeFromContainer()
|
||||
this.attachTabView(newTab)
|
||||
tab.parent = null
|
||||
newTab.parent = this
|
||||
this.recoveryStateChangedHint.next()
|
||||
this.onAfterTabAdded(newTab)
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves focus in the given direction
|
||||
*/
|
||||
@@ -498,6 +552,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
this.splitAdjusted.next(spanner)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
onTabDropped (tab: BaseTabComponent, zone: SplitDropZoneInfo) { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
|
||||
if (tab === this) {
|
||||
return
|
||||
}
|
||||
|
||||
this.add(tab, zone.relativeToTab, zone.side)
|
||||
this.tabAdopted.next(tab)
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
super.destroy()
|
||||
for (const x of this.getAllTabs()) {
|
||||
@@ -508,13 +572,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
layout (): void {
|
||||
this.root.normalize()
|
||||
this._spanners = []
|
||||
this._dropZones = []
|
||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||
}
|
||||
|
||||
private attachTabView (tab: BaseTabComponent) {
|
||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const ref = tab.insertIntoContainer(this.viewContainer)
|
||||
this.viewRefs.set(tab, ref)
|
||||
|
||||
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
||||
|
||||
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
|
||||
@@ -531,12 +595,12 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
})
|
||||
}
|
||||
|
||||
private detachTabView (tab: BaseTabComponent) {
|
||||
const ref = this.viewRefs.get(tab)
|
||||
if (ref) {
|
||||
this.viewRefs.delete(tab)
|
||||
this.viewContainer.remove(this.viewContainer.indexOf(ref))
|
||||
}
|
||||
private onAfterTabAdded (tab: BaseTabComponent) {
|
||||
setImmediate(() => {
|
||||
this.layout()
|
||||
this.tabAdded.next(tab)
|
||||
this.focus(tab)
|
||||
})
|
||||
}
|
||||
|
||||
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
|
||||
@@ -575,6 +639,13 @@ 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]
|
||||
@@ -594,6 +665,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
root.ratios = state.ratios
|
||||
root.children = children
|
||||
for (const childState of state.children) {
|
||||
if (!childState) {
|
||||
continue
|
||||
}
|
||||
if (childState.type === 'app:split-tab') {
|
||||
const child = new SplitContainer()
|
||||
await this.recoverContainer(child, childState, duplicate)
|
||||
@@ -618,7 +692,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SplitTabRecoveryProvider extends TabRecoveryProvider<SplitTabComponent> {
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:split-tab'
|
||||
|
37
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
37
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
:host {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: 5;
|
||||
padding: 15px;
|
||||
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||
|
||||
> div {
|
||||
flex: 1 1 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: rgba(255, 255, 255, .125);
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, .25);
|
||||
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
padding: 0px;
|
||||
border-radius: 3px;
|
||||
|
||||
> div {
|
||||
background: rgba(255, 255, 255, .5);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::ng-deep tab-header {
|
||||
// placeholders
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
69
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
69
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||
import { SplitDropZoneInfo } from './splitTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'split-tab-drop-zone',
|
||||
template: `
|
||||
<div
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="tabDropped.emit($event.item.data); isHighlighted = false"
|
||||
(cdkDropListEntered)="isHighlighted = true"
|
||||
(cdkDropListExited)="isHighlighted = false"
|
||||
cdkAutoDropGroup='app-tabs'
|
||||
>
|
||||
</div>
|
||||
`,
|
||||
styles: [require('./splitTabDropZone.component.scss')],
|
||||
})
|
||||
export class SplitTabDropZoneComponent extends SelfPositioningComponent {
|
||||
@Input() dropZone: SplitDropZoneInfo
|
||||
@Input() parent: BaseTabComponent
|
||||
@Output() tabDropped = new EventEmitter<BaseTabComponent>()
|
||||
@HostBinding('class.active') isActive = false
|
||||
@HostBinding('class.highlighted') isHighlighted = false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
element: ElementRef,
|
||||
app: AppService,
|
||||
) {
|
||||
super(element)
|
||||
this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
|
||||
this.isActive = !!tab && tab !== this.parent
|
||||
this.layout()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.layout()
|
||||
}
|
||||
|
||||
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'
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||
import { SplitContainer } from './splitTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@@ -8,20 +9,19 @@ import { SplitContainer } from './splitTab.component'
|
||||
template: '',
|
||||
styles: [require('./splitTabSpanner.component.scss')],
|
||||
})
|
||||
export class SplitTabSpannerComponent {
|
||||
export class SplitTabSpannerComponent extends SelfPositioningComponent {
|
||||
@Input() container: SplitContainer
|
||||
@Input() index: number
|
||||
@Output() change = new EventEmitter<void>()
|
||||
@HostBinding('class.active') isActive = false
|
||||
@HostBinding('class.h') isHorizontal = false
|
||||
@HostBinding('class.v') isVertical = true
|
||||
@HostBinding('style.left') cssLeft: string
|
||||
@HostBinding('style.top') cssTop: string
|
||||
@HostBinding('style.width') cssWidth: string | null
|
||||
@HostBinding('style.height') cssHeight: string | null
|
||||
private marginOffset = -5
|
||||
|
||||
constructor (private element: ElementRef) { }
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (element: ElementRef) {
|
||||
super(element)
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.element.nativeElement.addEventListener('dblclick', () => {
|
||||
@@ -92,11 +92,4 @@ export class SplitTabSpannerComponent {
|
||||
this.container.ratios[this.index] = ratio
|
||||
this.change.emit()
|
||||
}
|
||||
|
||||
private setDimensions (x: number, y: number, w: number, h: number) {
|
||||
this.cssLeft = `${x}%`
|
||||
this.cssTop = `${y}%`
|
||||
this.cssWidth = w ? `${w}%` : null
|
||||
this.cssHeight = h ? `${h}%` : null
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import { ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
export class StartPageComponent {
|
||||
version: string
|
||||
|
||||
private constructor (
|
||||
constructor (
|
||||
private config: ConfigService,
|
||||
private domSanitizer: DomSanitizer,
|
||||
public homeBase: HomeBaseService,
|
||||
|
@@ -6,9 +6,6 @@ import { BaseTabComponent } from '../components/baseTab.component'
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: `
|
||||
<!--perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
|
||||
<ng-template #scrollablePlaceholder></ng-template>
|
||||
</perfect-scrollbar-->
|
||||
<ng-template #placeholder></ng-template>
|
||||
`,
|
||||
styles: [
|
||||
@@ -30,6 +27,10 @@ export class TabBodyComponent implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
detach () {
|
||||
this.placeholder?.detach()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.placeholder?.detach()
|
||||
}
|
||||
|
@@ -2,9 +2,14 @@
|
||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||
.activity-indicator(*ngIf='tab.activity$|async')
|
||||
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex', #handle) {{index + 1}}
|
||||
ng-container(*ngIf='!config.store.terminal.hideTabIndex')
|
||||
.index(*ngIf='hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
|
||||
.index(*ngIf='hostApp.platform !== Platform.macOS') {{index + 1}}
|
||||
|
||||
.name(
|
||||
[title]='tab.customTitle || tab.title',
|
||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||
) {{tab.customTitle || tab.title}}
|
||||
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
||||
|
||||
ng-content
|
||||
|
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
@@ -13,11 +12,6 @@ import { BaseComponent } from './base.component'
|
||||
import { MenuItemOptions } from '../api/menu'
|
||||
import { PlatformService } from '../api/platform'
|
||||
|
||||
/** @hidden */
|
||||
export interface SortableComponentProxy {
|
||||
setDragHandle: (_: HTMLElement) => void
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
@@ -29,17 +23,16 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() progress: number|null
|
||||
@ViewChild('handle') handle?: ElementRef
|
||||
Platform = Platform
|
||||
|
||||
private constructor (
|
||||
constructor (
|
||||
public app: AppService,
|
||||
public config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
public hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private platform: PlatformService,
|
||||
private zone: NgZone,
|
||||
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
super()
|
||||
@@ -61,12 +54,6 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
if (this.handle && this.hostApp.platform === Platform.macOS) {
|
||||
this.parentDraggable.setDragHandle(this.handle.nativeElement)
|
||||
}
|
||||
}
|
||||
|
||||
showRenameTabModal (): void {
|
||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
|
@@ -5,7 +5,7 @@
|
||||
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
||||
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
||||
.main
|
||||
label {{transfer.getName()}}
|
||||
label.no-wrap([title]='transfer.getName()') {{transfer.getName()}}
|
||||
.status(*ngIf='transfer.isComplete()')
|
||||
ngb-progressbar(type='success', [value]='100')
|
||||
.status(*ngIf='transfer.isCancelled()')
|
||||
|
@@ -6,6 +6,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0 5px 25px;
|
||||
width: 100%;
|
||||
|
||||
.icon {
|
||||
padding: 4px 7px;
|
||||
@@ -16,12 +17,15 @@
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
margin-right: auto;
|
||||
margin-bottom: 3px;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import { AppService } from '../services/app.service'
|
||||
styles: [require('./windowControls.component.scss')],
|
||||
})
|
||||
export class WindowControlsComponent {
|
||||
private constructor (public hostWindow: HostWindowService, public app: AppService) { }
|
||||
constructor (public hostWindow: HostWindowService, public app: AppService) { }
|
||||
|
||||
async closeWindow () {
|
||||
this.app.closeWindow()
|
||||
|
@@ -69,6 +69,8 @@ hotkeys:
|
||||
pane-maximize:
|
||||
- 'Ctrl-Alt-Enter'
|
||||
close-pane: []
|
||||
switch-profile:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-T'
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -70,4 +70,6 @@ hotkeys:
|
||||
- '⌘-Shift-W'
|
||||
profile-selector:
|
||||
- '⌘-E'
|
||||
switch-profile:
|
||||
- '⌘-Shift-E'
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -70,6 +70,8 @@ hotkeys:
|
||||
pane-maximize:
|
||||
- 'Ctrl-Alt-Enter'
|
||||
close-pane: []
|
||||
switch-profile:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-T'
|
||||
pluginBlacklist: []
|
||||
|
@@ -16,6 +16,7 @@ appearance:
|
||||
vibrancyType: 'blur'
|
||||
terminal:
|
||||
showBuiltinProfiles: true
|
||||
showRecentProfiles: 3
|
||||
hotkeys:
|
||||
profile:
|
||||
__nonStructural: true
|
||||
@@ -30,3 +31,4 @@ enableAutomaticUpdates: true
|
||||
version: 1
|
||||
vault: null
|
||||
encrypted: false
|
||||
enableExperimentalFeatures: false
|
||||
|
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Directive, Input, OnInit } from '@angular/core'
|
||||
import { CdkDropList } from '@angular/cdk/drag-drop'
|
||||
|
||||
class FakeDropGroup {
|
||||
_items: Set<CdkDropList> = new Set()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Directive({
|
||||
selector: '[cdkAutoDropGroup]',
|
||||
})
|
||||
export class CdkAutoDropGroup implements OnInit {
|
||||
static groups: Record<string, FakeDropGroup> = {}
|
||||
|
||||
@Input('cdkAutoDropGroup') groupName: string
|
||||
|
||||
constructor (
|
||||
private cdkDropList: CdkDropList,
|
||||
) { }
|
||||
|
||||
ngOnInit (): void {
|
||||
CdkAutoDropGroup.groups[this.groupName] ??= new FakeDropGroup()
|
||||
CdkAutoDropGroup.groups[this.groupName]._items.add(this.cdkDropList)
|
||||
this.cdkDropList['_group'] = CdkAutoDropGroup.groups[this.groupName]
|
||||
}
|
||||
}
|
@@ -170,6 +170,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
id: 'pane-nav-next',
|
||||
name: 'Focus next pane',
|
||||
},
|
||||
{
|
||||
id: 'switch-profile',
|
||||
name: 'Switch profile in the active pane',
|
||||
},
|
||||
{
|
||||
id: 'close-pane',
|
||||
name: 'Close focused pane',
|
||||
|
@@ -5,7 +5,8 @@ import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
import { DndModule } from 'ng2-dnd'
|
||||
import { SortablejsModule } from 'ngx-sortablejs'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
|
||||
import { AppRootComponent } from './components/appRoot.component'
|
||||
import { CheckboxComponent } from './components/checkbox.component'
|
||||
@@ -21,6 +22,7 @@ import { RenameTabModalComponent } from './components/renameTabModal.component'
|
||||
import { SelectorModalComponent } from './components/selectorModal.component'
|
||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||
import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component'
|
||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||
@@ -29,8 +31,9 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
||||
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
||||
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService } from './api'
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
@@ -40,12 +43,12 @@ import { HotkeysService } from './services/hotkeys.service'
|
||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
import { AppHotkeyProvider } from './hotkeys'
|
||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu'
|
||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu, ProfilesContextMenu } from './tabContextMenu'
|
||||
import { LastCLIHandler, ProfileCLIHandler } from './cli'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SplitLayoutProfilesService } from './profiles'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
|
||||
const PROVIDERS = [
|
||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||
@@ -56,12 +59,14 @@ const PROVIDERS = [
|
||||
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: ProfilesContextMenu, multi: true },
|
||||
{ provide: TabRecoveryProvider, useExisting: SplitTabRecoveryProvider, multi: true },
|
||||
{ provide: CLIHandler, useClass: ProfileCLIHandler, multi: true },
|
||||
{ provide: CLIHandler, useClass: LastCLIHandler, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } },
|
||||
{ provide: FileProvider, useClass: VaultFileProvider, multi: true },
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: ProfileProvider, useExisting: SplitLayoutProfilesService, multi: true },
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
@@ -73,10 +78,11 @@ const PROVIDERS = [
|
||||
NgbModule,
|
||||
NgxFilesizeModule,
|
||||
PerfectScrollbarModule,
|
||||
DndModule.forRoot(),
|
||||
DragDropModule,
|
||||
SortablejsModule.forRoot({ animation: 150 }),
|
||||
],
|
||||
declarations: [
|
||||
AppRootComponent as any,
|
||||
AppRootComponent,
|
||||
CheckboxComponent,
|
||||
PromptModalComponent,
|
||||
StartPageComponent,
|
||||
@@ -93,10 +99,12 @@ const PROVIDERS = [
|
||||
SelectorModalComponent,
|
||||
SplitTabComponent,
|
||||
SplitTabSpannerComponent,
|
||||
SplitTabDropZoneComponent,
|
||||
UnlockVaultModalComponent,
|
||||
WelcomeTabComponent,
|
||||
TransfersMenuComponent,
|
||||
DropZoneDirective,
|
||||
CdkAutoDropGroup,
|
||||
],
|
||||
entryComponents: [
|
||||
PromptModalComponent,
|
||||
@@ -115,6 +123,8 @@ const PROVIDERS = [
|
||||
DropZoneDirective,
|
||||
FastHtmlBindDirective,
|
||||
AlwaysVisibleTypeaheadDirective,
|
||||
SortablejsModule,
|
||||
DragDropModule,
|
||||
],
|
||||
})
|
||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
@@ -126,9 +136,11 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
profilesService: ProfilesService,
|
||||
) {
|
||||
app.ready$.subscribe(() => {
|
||||
if (config.store.enableWelcomeTab) {
|
||||
app.openNewTabRaw({ type: WelcomeTabComponent })
|
||||
}
|
||||
config.ready$.toPromise().then(() => {
|
||||
if (config.store.enableWelcomeTab) {
|
||||
app.openNewTabRaw({ type: WelcomeTabComponent })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
platform.setErrorHandler(err => {
|
||||
|
57
tabby-core/src/profiles.ts
Normal file
57
tabby-core/src/profiles.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import slugify from 'slugify'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ConfigService, NewTabParameters, PartialProfile, Profile, ProfileProvider } from './api'
|
||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||
|
||||
export interface SplitLayoutProfileOptions {
|
||||
recoveryToken: any
|
||||
}
|
||||
|
||||
export interface SplitLayoutProfile extends Profile {
|
||||
options: SplitLayoutProfileOptions
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SplitLayoutProfilesService extends ProfileProvider<SplitLayoutProfile> {
|
||||
id = 'split-layout'
|
||||
name = 'Saved layout'
|
||||
configDefaults = {
|
||||
options: {
|
||||
recoveryToken: null,
|
||||
},
|
||||
}
|
||||
|
||||
constructor (
|
||||
private splitTabRecoveryProvider: SplitTabRecoveryProvider,
|
||||
private config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getBuiltinProfiles (): Promise<PartialProfile<SplitLayoutProfile>[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
async getNewTabParameters (profile: SplitLayoutProfile): Promise<NewTabParameters<SplitTabComponent>> {
|
||||
return this.splitTabRecoveryProvider.recover(profile.options.recoveryToken)
|
||||
}
|
||||
|
||||
getDescription (_: SplitLayoutProfile): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
async createProfile (tab: SplitTabComponent, name: string): Promise<void> {
|
||||
const token = await tab.getRecoveryToken()
|
||||
const profile: PartialProfile<SplitLayoutProfile> = {
|
||||
id: `${this.id}:custom:${slugify(name)}:${uuidv4()}`,
|
||||
type: this.id,
|
||||
name,
|
||||
options: {
|
||||
recoveryToken: token,
|
||||
},
|
||||
}
|
||||
this.config.store.profiles.push(profile)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
@@ -54,7 +54,9 @@ export class AppService {
|
||||
private activeTabChange = new Subject<BaseTabComponent|null>()
|
||||
private tabsChanged = new Subject<void>()
|
||||
private tabOpened = new Subject<BaseTabComponent>()
|
||||
private tabRemoved = new Subject<BaseTabComponent>()
|
||||
private tabClosed = new Subject<BaseTabComponent>()
|
||||
private tabDragActive = new Subject<BaseTabComponent|null>()
|
||||
private ready = new AsyncSubject<void>()
|
||||
|
||||
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
||||
@@ -62,7 +64,9 @@ export class AppService {
|
||||
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||
get tabDragActive$ (): Observable<BaseTabComponent|null> { return this.tabDragActive }
|
||||
|
||||
/** Fires once when the app is ready */
|
||||
get ready$ (): Observable<void> { return this.ready }
|
||||
@@ -131,21 +135,30 @@ export class AppService {
|
||||
})
|
||||
|
||||
tab.destroyed$.subscribe(() => {
|
||||
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
if (tab === this._activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged.next()
|
||||
this.removeTab(tab)
|
||||
this.tabRemoved.next(tab)
|
||||
this.tabClosed.next(tab)
|
||||
})
|
||||
|
||||
if (tab instanceof SplitTabComponent) {
|
||||
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
||||
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
||||
tab.tabAdopted$.subscribe(t => {
|
||||
this.removeTab(t)
|
||||
this.tabRemoved.next(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeTab (tab: BaseTabComponent): void {
|
||||
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
if (tab === this._activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged.next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
||||
* @param inputs Properties to be assigned on the new tab component instance
|
||||
@@ -344,6 +357,16 @@ export class AppService {
|
||||
this.hostApp.emitReady()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabDragStarted (tab: BaseTabComponent): void {
|
||||
this.tabDragActive.next(tab)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabDragEnded (): void {
|
||||
this.tabDragActive.next(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that fires once
|
||||
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import deepClone from 'clone-deep'
|
||||
import deepEqual from 'deep-equal'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||
@@ -8,7 +10,8 @@ import { HostAppService } from '../api/hostApp'
|
||||
import { Vault, VaultService } from './vault.service'
|
||||
const deepmerge = require('deepmerge')
|
||||
|
||||
const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
|
||||
const LATEST_VERSION = 1
|
||||
|
||||
@@ -18,7 +21,7 @@ function isStructuralMember (v) {
|
||||
}
|
||||
|
||||
function isNonStructuralObjectMember (v): boolean {
|
||||
return v instanceof Object && !(v instanceof Array) && v.__nonStructural
|
||||
return v instanceof Object && (v instanceof Array || v.__nonStructural)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@@ -46,49 +49,63 @@ export class ConfigProxy {
|
||||
{
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => this.getValue(key),
|
||||
get: () => this.__getValue(key),
|
||||
set: (value) => {
|
||||
this.setValue(key, value)
|
||||
this.__setValue(key, value)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.getValue = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
this.__getValue = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (real[key] !== undefined) {
|
||||
return real[key]
|
||||
} else {
|
||||
return this.getDefault(key)
|
||||
if (isNonStructuralObjectMember(defaults[key])) {
|
||||
// The object might be modified outside
|
||||
real[key] = this.__getDefault(key)
|
||||
delete real[key].__nonStructural
|
||||
return real[key]
|
||||
}
|
||||
return this.__getDefault(key)
|
||||
}
|
||||
}
|
||||
|
||||
this.getDefault = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (isNonStructuralObjectMember(defaults[key])) {
|
||||
real[key] = { ...defaults[key] }
|
||||
delete real[key].__nonStructural
|
||||
return real[key]
|
||||
} else {
|
||||
return defaults[key]
|
||||
}
|
||||
this.__getDefault = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
return deepClone(defaults[key])
|
||||
}
|
||||
|
||||
this.setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (value === this.getDefault(key)) {
|
||||
this.__setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (deepEqual(value, this.__getDefault(key))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete real[key]
|
||||
} else {
|
||||
real[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
this.__cleanup = () => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
// Trigger removal of default values
|
||||
for (const key in defaults) {
|
||||
if (isStructuralMember(defaults[key])) {
|
||||
this[key].__cleanup()
|
||||
} else {
|
||||
const v = this.__getValue(key)
|
||||
this.__setValue(key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
getValue (_key: string): any { }
|
||||
__getValue (_key: string): any { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
setValue (_key: string, _value: any) { }
|
||||
__setValue (_key: string, _value: any) { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
getDefault (_key: string): any { }
|
||||
__getDefault (_key: string): any { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__cleanup () { }
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -220,7 +237,7 @@ export class ConfigService {
|
||||
const module = imp.ngModule || imp
|
||||
if (module.ɵinj?.providers) {
|
||||
this.servicesCache[module.pluginName] = module.ɵinj.providers.map(provider => {
|
||||
return provider.useClass || provider
|
||||
return provider.useClass ?? provider.useExisting ?? provider
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -364,10 +381,12 @@ export class ConfigService {
|
||||
}
|
||||
delete decryptedVault.config.vault
|
||||
delete decryptedVault.config.encrypted
|
||||
delete decryptedVault.config.configSync
|
||||
return {
|
||||
...decryptedVault.config,
|
||||
vault: store.vault,
|
||||
encrypted: store.encrypted,
|
||||
configSync: store.configSync,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,9 +401,11 @@ export class ConfigService {
|
||||
vault.config = { ...store }
|
||||
delete vault.config.vault
|
||||
delete vault.config.encrypted
|
||||
delete vault.config.configSync
|
||||
return {
|
||||
vault: await this.vault.encrypt(vault),
|
||||
encrypted: true,
|
||||
configSync: store.configSync,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { Observable, Subject } from 'rxjs'
|
||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||
import { stringifyKeySequence, EventData } from './hotkeys.util'
|
||||
import { ConfigService } from './config.service'
|
||||
import { HostAppService, Platform } from '../api/hostApp'
|
||||
import { deprecate } from 'util'
|
||||
|
||||
export interface PartialHotkeyMatch {
|
||||
@@ -35,6 +36,7 @@ export class HotkeysService {
|
||||
private zone: NgZone,
|
||||
private config: ConfigService,
|
||||
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
|
||||
hostApp: HostAppService,
|
||||
) {
|
||||
const events = ['keydown', 'keyup']
|
||||
events.forEach(event => {
|
||||
@@ -43,6 +45,10 @@ export class HotkeysService {
|
||||
this.pushKeystroke(event, nativeEvent)
|
||||
this.processKeystrokes()
|
||||
this.emitKeyEvent(nativeEvent)
|
||||
if (hostApp.platform === Platform.Web) {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -53,7 +59,7 @@ export class HotkeysService {
|
||||
|
||||
// deprecated
|
||||
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
||||
this.matchedHotkey.subscribe = deprecate(s => this.matchedHotkey.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
|
||||
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +69,7 @@ export class HotkeysService {
|
||||
* @param nativeEvent event object
|
||||
*/
|
||||
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
|
||||
(nativeEvent as any).event = name
|
||||
nativeEvent['event'] = name
|
||||
this.currentKeystrokes.push({
|
||||
ctrlKey: nativeEvent.ctrlKey,
|
||||
metaKey: nativeEvent.metaKey,
|
||||
|
@@ -1,25 +1,46 @@
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { NewTabParameters } from './tabs.service'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Profile, ProfileProvider } from '../api/profileProvider'
|
||||
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
|
||||
import { SelectorOption } from '../api/selector'
|
||||
import { AppService } from './app.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { configMerge, ConfigProxy, ConfigService } from './config.service'
|
||||
import { NotificationsService } from './notifications.service'
|
||||
import { SelectorService } from './selector.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProfilesService {
|
||||
private profileDefaults = {
|
||||
id: '',
|
||||
type: '',
|
||||
name: '',
|
||||
group: '',
|
||||
options: {},
|
||||
icon: '',
|
||||
color: '',
|
||||
disableDynamicTitle: false,
|
||||
weight: 0,
|
||||
isBuiltin: false,
|
||||
isTemplate: false,
|
||||
}
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider[],
|
||||
private notifications: NotificationsService,
|
||||
private selector: SelectorService,
|
||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
||||
) { }
|
||||
|
||||
async openNewTabForProfile (profile: Profile): Promise<BaseTabComponent|null> {
|
||||
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
|
||||
const params = await this.newTabParametersForProfile(profile)
|
||||
if (params) {
|
||||
const tab = this.app.openNewTab(params)
|
||||
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
||||
tab.setTitle(profile.name)
|
||||
|
||||
if (profile.name) {
|
||||
tab.setTitle(profile.name)
|
||||
}
|
||||
if (profile.disableDynamicTitle) {
|
||||
tab['enableDynamicTitle'] = false
|
||||
}
|
||||
@@ -28,36 +49,137 @@ export class ProfilesService {
|
||||
return null
|
||||
}
|
||||
|
||||
async newTabParametersForProfile (profile: Profile): Promise<NewTabParameters<BaseTabComponent>|null> {
|
||||
return this.providerForProfile(profile)?.getNewTabParameters(profile) ?? null
|
||||
async newTabParametersForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>|null> {
|
||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||
return this.providerForProfile(fullProfile)?.getNewTabParameters(fullProfile) ?? null
|
||||
}
|
||||
|
||||
getProviders (): ProfileProvider[] {
|
||||
getProviders (): ProfileProvider<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
}
|
||||
|
||||
async getProfiles (): Promise<Profile[]> {
|
||||
async getProfiles (): Promise<PartialProfile<Profile>[]> {
|
||||
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
||||
let list = lists.reduce((a, b) => a.concat(b), [])
|
||||
list = [
|
||||
...this.config.store.profiles ?? [],
|
||||
...list,
|
||||
]
|
||||
list.sort((a, b) => a.group?.localeCompare(b.group ?? '') ?? -1)
|
||||
list.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const sortKey = p => `${p.group ?? ''} / ${p.name}`
|
||||
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
|
||||
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
|
||||
return list
|
||||
}
|
||||
|
||||
providerForProfile (profile: Profile): ProfileProvider|null {
|
||||
return this.profileProviders.find(x => x.id === profile.type) ?? null
|
||||
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
|
||||
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
|
||||
return provider as unknown as ProfileProvider<T>|null
|
||||
}
|
||||
|
||||
selectorOptionForProfile <T> (profile: Profile): SelectorOption<T> {
|
||||
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
||||
profile = this.getConfigProxyForProfile(profile)
|
||||
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||
}
|
||||
|
||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||
return {
|
||||
icon: profile.icon,
|
||||
name: profile.group ? `${profile.group} / ${profile.name}` : profile.name,
|
||||
description: this.providerForProfile(profile)?.getDescription(profile),
|
||||
name: profile.group ? `${fullProfile.group} / ${fullProfile.name}` : fullProfile.name,
|
||||
description: this.providerForProfile(fullProfile)?.getDescription(fullProfile),
|
||||
}
|
||||
}
|
||||
|
||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||
return new Promise<PartialProfile<Profile>|null>(async (resolve, reject) => {
|
||||
try {
|
||||
let recentProfiles: PartialProfile<Profile>[] = this.config.store.recentProfiles
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
|
||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
icon: 'fas fa-history',
|
||||
callback: async () => {
|
||||
if (p.id) {
|
||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||
}
|
||||
resolve(p)
|
||||
},
|
||||
}))
|
||||
if (recentProfiles.length) {
|
||||
options.push({
|
||||
name: 'Clear recent connections',
|
||||
icon: 'fas fa-eraser',
|
||||
callback: async () => {
|
||||
this.config.store.recentProfiles = []
|
||||
this.config.save()
|
||||
resolve(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let profiles = await this.getProfiles()
|
||||
|
||||
if (!this.config.store.terminal.showBuiltinProfiles) {
|
||||
profiles = profiles.filter(x => !x.isBuiltin)
|
||||
}
|
||||
|
||||
profiles = profiles.filter(x => !x.isTemplate)
|
||||
|
||||
options = [...options, ...profiles.map((p): SelectorOption<void> => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
callback: () => resolve(p),
|
||||
}))]
|
||||
|
||||
try {
|
||||
const { SettingsTabComponent } = window['nodeRequire']('tabby-settings')
|
||||
options.push({
|
||||
name: 'Manage profiles',
|
||||
icon: 'fas fa-window-restore',
|
||||
callback: () => {
|
||||
this.app.openNewTabRaw({
|
||||
type: SettingsTabComponent,
|
||||
inputs: { activeTab: 'profiles' },
|
||||
})
|
||||
resolve(null)
|
||||
},
|
||||
})
|
||||
} catch { }
|
||||
|
||||
if (this.getProviders().some(x => x.supportsQuickConnect)) {
|
||||
options.push({
|
||||
name: 'Quick connect',
|
||||
freeInputPattern: 'Connect to "%s"...',
|
||||
icon: 'fas fa-arrow-right',
|
||||
callback: query => {
|
||||
const profile = this.quickConnect(query)
|
||||
resolve(profile)
|
||||
},
|
||||
})
|
||||
}
|
||||
await this.selector.show('Select profile or enter an address', options)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
|
||||
for (const provider of this.getProviders()) {
|
||||
if (provider.supportsQuickConnect) {
|
||||
const profile = provider.quickConnect(query)
|
||||
if (profile) {
|
||||
return profile
|
||||
}
|
||||
}
|
||||
}
|
||||
this.notifications.error(`Could not parse "${query}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>): T {
|
||||
const provider = this.providerForProfile(profile)
|
||||
const defaults = configMerge(this.profileDefaults, provider?.configDefaults ?? {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export class TabRecoveryService {
|
||||
}
|
||||
|
||||
async saveTabs (tabs: BaseTabComponent[]): Promise<void> {
|
||||
if (!this.enabled) {
|
||||
if (!this.enabled || !this.config.store.recoverTabs) {
|
||||
return
|
||||
}
|
||||
window.localStorage.tabsRecovery = JSON.stringify(
|
||||
|
@@ -26,15 +26,25 @@ interface StoredVault {
|
||||
|
||||
export interface VaultSecret {
|
||||
type: string
|
||||
key: Record<string, any>
|
||||
key: VaultSecretKey
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface VaultFileSecret extends VaultSecret {
|
||||
key: {
|
||||
id: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface Vault {
|
||||
config: any
|
||||
secrets: VaultSecret[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface VaultSecretKey { }
|
||||
|
||||
function migrateVaultContent (content: any): Vault {
|
||||
return {
|
||||
config: content.config,
|
||||
@@ -121,6 +131,10 @@ export class VaultService {
|
||||
return !!_rememberedPassphrase
|
||||
}
|
||||
|
||||
forgetPassphrase (): void {
|
||||
_rememberedPassphrase = null
|
||||
}
|
||||
|
||||
async decrypt (storage: StoredVault, passphrase?: string): Promise<Vault> {
|
||||
if (!passphrase) {
|
||||
passphrase = await this.getPassphrase()
|
||||
@@ -128,7 +142,7 @@ export class VaultService {
|
||||
try {
|
||||
return await wrapPromise(this.zone, decryptVault(storage, passphrase))
|
||||
} catch (e) {
|
||||
_rememberedPassphrase = null
|
||||
this.forgetPassphrase()
|
||||
if (e.toString().includes('BAD_DECRYPT')) {
|
||||
this.notifications.error('Incorrect passphrase')
|
||||
}
|
||||
@@ -173,7 +187,7 @@ export class VaultService {
|
||||
return _rememberedPassphrase!
|
||||
}
|
||||
|
||||
async getSecret (type: string, key: Record<string, any>): Promise<VaultSecret|null> {
|
||||
async getSecret (type: string, key: VaultSecretKey): Promise<VaultSecret|null> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
@@ -193,7 +207,21 @@ export class VaultService {
|
||||
await this.save(vault)
|
||||
}
|
||||
|
||||
async removeSecret (type: string, key: Record<string, any>): Promise<void> {
|
||||
async updateSecret (secret: VaultSecret, update: VaultSecret): Promise<void> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
return
|
||||
}
|
||||
const target = vault.secrets.find(s => s.type === secret.type && this.keyMatches(secret.key, s))
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
Object.assign(target, update)
|
||||
await this.save(vault)
|
||||
}
|
||||
|
||||
async removeSecret (type: string, key: VaultSecretKey): Promise<void> {
|
||||
await this.ready$.toPromise()
|
||||
const vault = await this.load()
|
||||
if (!vault) {
|
||||
@@ -203,7 +231,7 @@ export class VaultService {
|
||||
await this.save(vault)
|
||||
}
|
||||
|
||||
private keyMatches (key: Record<string, any>, secret: VaultSecret): boolean {
|
||||
private keyMatches (key: VaultSecretKey, secret: VaultSecret): boolean {
|
||||
return Object.keys(key).every(k => secret.key[k] === key[k])
|
||||
}
|
||||
|
||||
@@ -242,9 +270,9 @@ export class VaultFileProvider extends FileProvider {
|
||||
if (!vault) {
|
||||
throw new Error('Vault is locked')
|
||||
}
|
||||
const files = vault.secrets.filter(x => x.type === VAULT_SECRET_TYPE_FILE)
|
||||
const files = vault.secrets.filter(x => x.type === VAULT_SECRET_TYPE_FILE) as VaultFileSecret[]
|
||||
if (files.length) {
|
||||
const result = await this.selector.show<VaultSecret|null>('Select file', [
|
||||
const result = await this.selector.show<VaultFileSecret|null>('Select file', [
|
||||
{
|
||||
name: 'Add a new file',
|
||||
icon: 'fas fa-plus',
|
||||
@@ -274,7 +302,7 @@ export class VaultFileProvider extends FileProvider {
|
||||
type: VAULT_SECRET_TYPE_FILE,
|
||||
key: {
|
||||
id,
|
||||
description,
|
||||
description: `${description} (${transfer.getName()})`,
|
||||
},
|
||||
value: (await transfer.readAll()).toString('base64'),
|
||||
})
|
||||
|
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { AppService } from './services/app.service'
|
||||
import { BaseTabComponent } from './components/baseTab.component'
|
||||
@@ -7,6 +8,11 @@ import { TabHeaderComponent } from './components/tabHeader.component'
|
||||
import { SplitTabComponent, SplitDirection } from './components/splitTab.component'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
import { MenuItemOptions } from './api/menu'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { TabsService } from './services/tabs.service'
|
||||
import { HotkeysService } from './services/hotkeys.service'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SplitLayoutProfilesService } from './profiles'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -100,6 +106,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private ngbModal: NgbModal,
|
||||
private splitLayoutProfilesService: SplitLayoutProfilesService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -130,6 +138,21 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
})) as MenuItemOptions[],
|
||||
},
|
||||
]
|
||||
|
||||
if (tab instanceof SplitTabComponent && tab.getAllTabs().length > 1) {
|
||||
items.push({
|
||||
label: 'Save layout as profile',
|
||||
click: async () => {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'Profile name'
|
||||
const name = (await modal.result)?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
this.splitLayoutProfilesService.createProfile(tab, name)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
@@ -203,3 +226,65 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ProfilesContextMenu extends TabContextMenuItemProvider {
|
||||
weight = 10
|
||||
|
||||
constructor (
|
||||
private profilesService: ProfilesService,
|
||||
private tabsService: TabsService,
|
||||
private app: AppService,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
hotkeys.hotkey$.subscribe(hotkey => {
|
||||
if (hotkey === 'switch-profile') {
|
||||
let tab = this.app.activeTab
|
||||
if (tab instanceof SplitTabComponent) {
|
||||
tab = tab.getFocusedTab()
|
||||
if (tab) {
|
||||
this.switchTabProfile(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async switchTabProfile (tab: BaseTabComponent) {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
if (!profile) {
|
||||
return
|
||||
}
|
||||
|
||||
const params = await this.profilesService.newTabParametersForProfile(profile)
|
||||
if (!params) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!await tab.canClose()) {
|
||||
return
|
||||
}
|
||||
|
||||
const newTab = this.tabsService.create(params)
|
||||
;(tab.parent as SplitTabComponent).replaceTab(tab, newTab)
|
||||
|
||||
tab.destroy()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
|
||||
if (!tabHeader && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) {
|
||||
return [
|
||||
{
|
||||
label: 'Switch profile',
|
||||
click: () => this.switchTabProfile(tab),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
@@ -90,10 +90,6 @@ $list-group-border-radius: 0;
|
||||
$pre-bg: $dropdown-bg;
|
||||
$pre-color: $dropdown-link-color;
|
||||
|
||||
$alert-danger-bg: $body-bg;
|
||||
$alert-danger-text: $red;
|
||||
$alert-danger-border: $red;
|
||||
|
||||
$headings-font-weight: lighter;
|
||||
$headings-color: $base0;
|
||||
|
||||
|
@@ -243,7 +243,8 @@ hotkey-input-modal {
|
||||
.list-group-item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, .05);
|
||||
|
||||
border-top: 1px solid rgba(0, 21, 43, .4);
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
@@ -306,21 +307,6 @@ search-panel {
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-outline-secondary {
|
||||
@include button-outline-variant(#9badb9, #fff);
|
||||
&:hover:not([disabled]), &:active:not([disabled]), &.active:not([disabled]) {
|
||||
background-color: #3f484e;
|
||||
border-color: darken(#9badb9, 25%);
|
||||
}
|
||||
|
||||
border-color: darken(#9badb9, 25%);
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
color: #9badb9;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning:not(:disabled):not(.disabled) {
|
||||
&.active, &:active {
|
||||
color: $gray-900;
|
||||
|
@@ -193,3 +193,7 @@ $modal-content-border-width: 0;
|
||||
|
||||
$progress-bg: $table-bg;
|
||||
$progress-height: 3px;
|
||||
|
||||
$alert-bg-level: 9;
|
||||
$alert-border-level: 5;
|
||||
$alert-color-level: -5;
|
||||
|
@@ -374,11 +374,6 @@ ms@2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ng2-dnd@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ng2-dnd/-/ng2-dnd-5.0.2.tgz#862278ac7dedfa14f5783bbf34014d5d73dfefb4"
|
||||
integrity sha512-5mWWBePwvEPsNd/HkdbD543Q9mPyJofL6zkNydl8/Ah3qrrvZT2DaEPbknY08OgkXpI2qUGksc01OzzVlRQ9dQ==
|
||||
|
||||
ngx-filesize@^2.0.16:
|
||||
version "2.0.16"
|
||||
resolved "https://registry.yarnpkg.com/ngx-filesize/-/ngx-filesize-2.0.16.tgz#fdaba04170edb6cfcdf7be932783cf913b03f016"
|
||||
|
1
tabby-electron/.gitignore
vendored
1
tabby-electron/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-electron",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Electron-specific bindings",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -20,7 +20,6 @@
|
||||
"@angular/core": "^9.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"winston": "^3.3.3",
|
||||
"electron-promise-ipc": "^2.2.4"
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage } from 'electron'
|
||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage, PowerSaveBlocker } from 'electron'
|
||||
import * as remote from '@electron/remote'
|
||||
|
||||
export interface MessageBoxResponse {
|
||||
@@ -20,6 +20,7 @@ export class ElectronService {
|
||||
remote: Remote
|
||||
process: any
|
||||
autoUpdater: AutoUpdater
|
||||
powerSaveBlocker: PowerSaveBlocker
|
||||
TouchBar: typeof TouchBar
|
||||
BrowserWindow: typeof BrowserWindow
|
||||
Menu: typeof Menu
|
||||
@@ -39,6 +40,7 @@ export class ElectronService {
|
||||
this.globalShortcut = remote.globalShortcut
|
||||
this.nativeImage = remote.nativeImage
|
||||
this.autoUpdater = remote.autoUpdater
|
||||
this.powerSaveBlocker = remote.powerSaveBlocker
|
||||
this.TouchBar = remote.TouchBar
|
||||
this.BrowserWindow = remote.BrowserWindow
|
||||
this.Menu = remote.Menu
|
||||
|
@@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as fsSync from 'fs'
|
||||
import * as os from 'os'
|
||||
import promiseIpc from 'electron-promise-ipc'
|
||||
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
||||
import { execFile } from 'mz/child_process'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { PlatformService, ClipboardContent, HostAppService, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'tabby-core'
|
||||
@@ -49,11 +49,11 @@ export class ElectronPlatformService extends PlatformService {
|
||||
}
|
||||
|
||||
async installPlugin (name: string, version: string): Promise<void> {
|
||||
await (promiseIpc as any).send('plugin-manager:install', name, version)
|
||||
await (promiseIpc as RendererProcessType).send('plugin-manager:install', name, version)
|
||||
}
|
||||
|
||||
async uninstallPlugin (name: string): Promise<void> {
|
||||
await (promiseIpc as any).send('plugin-manager:uninstall', name)
|
||||
await (promiseIpc as RendererProcessType).send('plugin-manager:uninstall', name)
|
||||
}
|
||||
|
||||
async isProcessRunning (name: string): Promise<boolean> {
|
||||
@@ -198,14 +198,14 @@ export class ElectronPlatformService extends PlatformService {
|
||||
}
|
||||
|
||||
return Promise.all(result.filePaths.map(async p => {
|
||||
const transfer = new ElectronFileUpload(p)
|
||||
const transfer = new ElectronFileUpload(p, this.electron)
|
||||
await wrapPromise(this.zone, transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
return transfer
|
||||
}))
|
||||
}
|
||||
|
||||
async startDownload (name: string, size: number): Promise<FileDownload|null> {
|
||||
async startDownload (name: string, mode: number, size: number): Promise<FileDownload|null> {
|
||||
const result = await this.electron.dialog.showSaveDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
@@ -215,7 +215,7 @@ export class ElectronPlatformService extends PlatformService {
|
||||
if (!result.filePath) {
|
||||
return null
|
||||
}
|
||||
const transfer = new ElectronFileDownload(result.filePath, size)
|
||||
const transfer = new ElectronFileDownload(result.filePath, mode, size, this.electron)
|
||||
await wrapPromise(this.zone, transfer.open())
|
||||
this.fileTransferStarted.next(transfer)
|
||||
return transfer
|
||||
@@ -230,16 +230,21 @@ export class ElectronPlatformService extends PlatformService {
|
||||
|
||||
class ElectronFileUpload extends FileUpload {
|
||||
private size: number
|
||||
private mode: number
|
||||
private file: fs.FileHandle
|
||||
private buffer: Buffer
|
||||
private powerSaveBlocker = 0
|
||||
|
||||
constructor (private filePath: string) {
|
||||
constructor (private filePath: string, private electron: ElectronService) {
|
||||
super()
|
||||
this.buffer = Buffer.alloc(256 * 1024)
|
||||
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
this.size = (await fs.stat(this.filePath)).size
|
||||
const stat = await fs.stat(this.filePath)
|
||||
this.size = stat.size
|
||||
this.mode = stat.mode
|
||||
this.file = await fs.open(this.filePath, 'r')
|
||||
}
|
||||
|
||||
@@ -247,6 +252,10 @@ class ElectronFileUpload extends FileUpload {
|
||||
return path.basename(this.filePath)
|
||||
}
|
||||
|
||||
getMode (): number {
|
||||
return this.mode
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.size
|
||||
}
|
||||
@@ -258,28 +267,37 @@ class ElectronFileUpload extends FileUpload {
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||
this.file.close()
|
||||
}
|
||||
}
|
||||
|
||||
class ElectronFileDownload extends FileDownload {
|
||||
private file: fs.FileHandle
|
||||
private powerSaveBlocker = 0
|
||||
|
||||
constructor (
|
||||
private filePath: string,
|
||||
private mode: number,
|
||||
private size: number,
|
||||
private electron: ElectronService,
|
||||
) {
|
||||
super()
|
||||
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
this.file = await fs.open(this.filePath, 'w')
|
||||
this.file = await fs.open(this.filePath, 'w', this.mode)
|
||||
}
|
||||
|
||||
getName (): string {
|
||||
return path.basename(this.filePath)
|
||||
}
|
||||
|
||||
getMode (): number {
|
||||
return this.mode
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.size
|
||||
}
|
||||
@@ -294,6 +312,7 @@ class ElectronFileDownload extends FileDownload {
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||
this.file.close()
|
||||
}
|
||||
}
|
||||
|
@@ -16,13 +16,6 @@ async@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||
dependencies:
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
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"
|
||||
@@ -150,11 +143,6 @@ 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==
|
||||
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
|
1
tabby-local/.gitignore
vendored
1
tabby-local/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-local",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Tabby's local shell plugin",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -22,13 +22,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/deep-equal": "^1.0.0",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"dataurl": "0.1.0",
|
||||
"deep-equal": "2.0.5",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"shell-escape": "^0.2.0",
|
||||
"utils-decorators": "^1.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
41
tabby-local/src/components/commandLineEditor.component.pug
Normal file
41
tabby-local/src/components/commandLineEditor.component.pug
Normal file
@@ -0,0 +1,41 @@
|
||||
ng-container(*ngIf='!argvMode')
|
||||
.form-group
|
||||
label Command line
|
||||
.input-group
|
||||
.input-group-prepend
|
||||
button.btn.btn-secondary((click)='switchToArgv()', title='Switch to split arguments')
|
||||
i.fas.fa-fw.fa-caret-right
|
||||
input.form-control.text-monospace(
|
||||
[(ngModel)]='command',
|
||||
(ngModelChange)='parseCommand()'
|
||||
)
|
||||
|
||||
ng-container(*ngIf='argvMode')
|
||||
.form-group
|
||||
label Program
|
||||
.input-group
|
||||
.input-group-prepend
|
||||
button.btn.btn-secondary((click)='switchToCommand()', title='Switch to a single-line command')
|
||||
i.fas.fa-fw.fa-caret-down
|
||||
input.form-control.text-monospace(
|
||||
type='text',
|
||||
[(ngModel)]='_model.command',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Arguments
|
||||
.input-group(
|
||||
*ngFor='let arg of _model.args; index as i; trackBy: trackByIndex',
|
||||
)
|
||||
input.form-control.text-monospace(
|
||||
type='text',
|
||||
[(ngModel)]='_model.args[i]',
|
||||
)
|
||||
.input-group-append
|
||||
button.btn.btn-secondary((click)='_model.args.splice(i, 1)')
|
||||
i.fas.fa-fw.fa-trash
|
||||
|
||||
.mt-2
|
||||
button.btn.btn-secondary((click)='_model.args.push("")')
|
||||
i.fas.fa-plus.mr-2
|
||||
| Add
|
50
tabby-local/src/components/commandLineEditor.component.ts
Normal file
50
tabby-local/src/components/commandLineEditor.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import * as shellQuote from 'shell-quote'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { SessionOptions } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'command-line-editor',
|
||||
template: require('./commandLineEditor.component.pug'),
|
||||
})
|
||||
export class CommandLineEditorComponent {
|
||||
@Input() argvMode = false
|
||||
@Input() _model: SessionOptions
|
||||
command = ''
|
||||
|
||||
@Input() get model (): SessionOptions {
|
||||
return this._model
|
||||
}
|
||||
|
||||
set model (value: SessionOptions) {
|
||||
this._model = value
|
||||
this.updateCommand()
|
||||
}
|
||||
|
||||
switchToCommand () {
|
||||
this.updateCommand()
|
||||
this.argvMode = false
|
||||
}
|
||||
|
||||
switchToArgv () {
|
||||
this.argvMode = true
|
||||
}
|
||||
|
||||
parseCommand () {
|
||||
const args = shellQuote.parse(this.command)
|
||||
this.model.command = args[0] ?? ''
|
||||
this.model.args = args.slice(1)
|
||||
}
|
||||
|
||||
updateCommand () {
|
||||
this.command = shellQuote.quote([
|
||||
this.model.command,
|
||||
...this.model.args ?? [],
|
||||
])
|
||||
}
|
||||
|
||||
trackByIndex (index) {
|
||||
return index
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
|
||||
.input-group
|
||||
input.form-control.w-25([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
|
||||
input.form-control.w-25.text-monospace([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
|
||||
.input-group-append
|
||||
.input-group-text =
|
||||
input.form-control.w-50([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
|
||||
input.form-control.w-50.text-monospace([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
|
||||
.input-group-append
|
||||
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
|
||||
i.fas.fa-trash
|
||||
i.fas.fa-fw.fa-trash
|
||||
|
||||
button.btn.btn-secondary((click)='addEnvironmentVar()')
|
||||
i.fas.fa-plus.mr-2
|
||||
|
@@ -1,27 +1,4 @@
|
||||
.form-group
|
||||
label Command
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='profile.options.command',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Arguments
|
||||
.input-group(
|
||||
*ngFor='let arg of profile.options.args; index as i; trackBy: trackByIndex',
|
||||
)
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='profile.options.args[i]',
|
||||
)
|
||||
.input-group-append
|
||||
button.btn.btn-secondary((click)='profile.options.args.splice(i, 1)')
|
||||
i.fas.fa-trash
|
||||
|
||||
.mt-2
|
||||
button.btn.btn-secondary((click)='profile.options.args.push("")')
|
||||
i.fas.fa-plus.mr-2
|
||||
| Add
|
||||
command-line-editor([model]='profile.options')
|
||||
|
||||
.form-line(*ngIf='uac.isAvailable')
|
||||
.header
|
||||
|
@@ -3,14 +3,14 @@ import { Component } from '@angular/core'
|
||||
import { UACService } from '../services/uac.service'
|
||||
import { LocalProfile } from '../api'
|
||||
import { ElectronHostWindow, ElectronService } from 'tabby-electron'
|
||||
import { ProfileSettingsComponent } from '../../../tabby-core/src/api/profileProvider'
|
||||
import { ProfileSettingsComponent } from 'tabby-core'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./localProfileSettings.component.pug'),
|
||||
})
|
||||
export class LocalProfileSettingsComponent implements ProfileSettingsComponent {
|
||||
export class LocalProfileSettingsComponent implements ProfileSettingsComponent<LocalProfile> {
|
||||
profile: LocalProfile
|
||||
|
||||
constructor (
|
||||
@@ -40,8 +40,4 @@ export class LocalProfileSettingsComponent implements ProfileSettingsComponent {
|
||||
)).filePaths
|
||||
this.profile.options.cwd = paths[0]
|
||||
}
|
||||
|
||||
trackByIndex (index) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, Input, Injector } from '@angular/core'
|
||||
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { SessionOptions } from '../api'
|
||||
import { LocalProfile, SessionOptions } from '../api'
|
||||
import { Session } from '../session'
|
||||
import { UACService } from '../services/uac.service'
|
||||
|
||||
@@ -13,7 +13,8 @@ import { UACService } from '../services/uac.service'
|
||||
animations: BaseTerminalTabComponent.animations,
|
||||
})
|
||||
export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
@Input() sessionOptions: SessionOptions
|
||||
@Input() sessionOptions: SessionOptions // Deprecated
|
||||
@Input() profile: LocalProfile
|
||||
session: Session|null = null
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
@@ -25,6 +26,8 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
}
|
||||
|
||||
ngOnInit (): void {
|
||||
this.sessionOptions = this.profile.options
|
||||
|
||||
this.logger = this.log.create('terminalTab')
|
||||
this.session = new Session(this.injector)
|
||||
|
||||
@@ -49,17 +52,17 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
protected onFrontendReady (): void {
|
||||
this.initializeSession(this.size.columns, this.size.rows)
|
||||
this.savedStateIsLive = this.sessionOptions.restoreFromPTYID === this.session?.getPTYID()
|
||||
this.savedStateIsLive = this.profile.options.restoreFromPTYID === this.session?.getPTYID()
|
||||
super.onFrontendReady()
|
||||
}
|
||||
|
||||
initializeSession (columns: number, rows: number): void {
|
||||
if (this.sessionOptions.runAsAdministrator && this.uac.isAvailable) {
|
||||
this.sessionOptions = this.uac.patchSessionOptionsForUAC(this.sessionOptions)
|
||||
if (this.profile.options.runAsAdministrator && this.uac.isAvailable) {
|
||||
this.profile.options = this.uac.patchSessionOptionsForUAC(this.profile.options)
|
||||
}
|
||||
|
||||
this.session!.start({
|
||||
...this.sessionOptions,
|
||||
...this.profile.options,
|
||||
width: columns,
|
||||
height: rows,
|
||||
})
|
||||
@@ -71,11 +74,14 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
const cwd = this.session ? await this.session.getWorkingDirectory() : null
|
||||
return {
|
||||
type: 'app:terminal-tab',
|
||||
sessionOptions: {
|
||||
...this.sessionOptions,
|
||||
cwd: cwd ?? this.sessionOptions.cwd,
|
||||
restoreFromPTYID: this.session?.getPTYID(),
|
||||
type: 'app:local-tab',
|
||||
profile: {
|
||||
...this.profile,
|
||||
options: {
|
||||
...this.profile.options,
|
||||
cwd: cwd ?? this.profile.options.cwd,
|
||||
restoreFromPTYID: this.session?.getPTYID(),
|
||||
},
|
||||
},
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
|
@@ -3,9 +3,6 @@ import { ConfigProvider, Platform } from 'tabby-core'
|
||||
/** @hidden */
|
||||
export class TerminalConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
hotkeys: {
|
||||
'copy-current-path': [],
|
||||
},
|
||||
terminal: {
|
||||
autoOpen: false,
|
||||
useConPTY: true,
|
||||
|
@@ -13,6 +13,7 @@ import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
|
||||
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
|
||||
import { LocalProfileSettingsComponent } from './components/localProfileSettings.component'
|
||||
import { CommandLineEditorComponent } from './components/commandLineEditor.component'
|
||||
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
import { DockMenuService } from './services/dockMenu.service'
|
||||
@@ -91,16 +92,18 @@ import { LocalProfilesService } from './profiles'
|
||||
TerminalTabComponent,
|
||||
ShellSettingsTabComponent,
|
||||
LocalProfileSettingsComponent,
|
||||
] as any[],
|
||||
],
|
||||
declarations: [
|
||||
TerminalTabComponent,
|
||||
ShellSettingsTabComponent,
|
||||
EnvironmentEditorComponent,
|
||||
CommandLineEditorComponent,
|
||||
LocalProfileSettingsComponent,
|
||||
] as any[],
|
||||
],
|
||||
exports: [
|
||||
TerminalTabComponent,
|
||||
EnvironmentEditorComponent,
|
||||
CommandLineEditorComponent,
|
||||
],
|
||||
})
|
||||
export default class LocalTerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
@@ -1,14 +1,30 @@
|
||||
import deepClone from 'clone-deep'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { ProfileProvider, Profile, NewTabParameters, ConfigService, SplitTabComponent, AppService } from 'tabby-core'
|
||||
import { ProfileProvider, NewTabParameters, ConfigService, SplitTabComponent, AppService, PartialProfile } from 'tabby-core'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { LocalProfileSettingsComponent } from './components/localProfileSettings.component'
|
||||
import { ShellProvider, Shell, SessionOptions } from './api'
|
||||
import { ShellProvider, Shell, SessionOptions, LocalProfile } from './api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LocalProfilesService extends ProfileProvider {
|
||||
export class LocalProfilesService extends ProfileProvider<LocalProfile> {
|
||||
id = 'local'
|
||||
name = 'Local'
|
||||
settingsComponent = LocalProfileSettingsComponent
|
||||
configDefaults = {
|
||||
options: {
|
||||
restoreFromPTYID: null,
|
||||
command: '',
|
||||
args: [],
|
||||
cwd: null,
|
||||
env: {
|
||||
__nonStructural: true,
|
||||
},
|
||||
width: null,
|
||||
height: null,
|
||||
pauseAfterExit: false,
|
||||
runAsAdministrator: false,
|
||||
},
|
||||
}
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
@@ -18,7 +34,7 @@ export class LocalProfilesService extends ProfileProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getBuiltinProfiles (): Promise<Profile[]> {
|
||||
async getBuiltinProfiles (): Promise<PartialProfile<LocalProfile>[]> {
|
||||
return (await this.getShells()).map(shell => ({
|
||||
id: `local:${shell.id}`,
|
||||
type: 'local',
|
||||
@@ -29,18 +45,20 @@ export class LocalProfilesService extends ProfileProvider {
|
||||
}))
|
||||
}
|
||||
|
||||
async getNewTabParameters (profile: Profile): Promise<NewTabParameters<TerminalTabComponent>> {
|
||||
const options = { ...profile.options }
|
||||
async getNewTabParameters (profile: PartialProfile<LocalProfile>): Promise<NewTabParameters<TerminalTabComponent>> {
|
||||
profile = deepClone(profile)
|
||||
|
||||
if (!options.cwd) {
|
||||
if (!profile.options?.cwd) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
|
||||
options.cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
profile.options ??= {}
|
||||
profile.options.cwd = await this.app.activeTab.session.getWorkingDirectory() ?? undefined
|
||||
}
|
||||
if (this.app.activeTab instanceof SplitTabComponent) {
|
||||
const focusedTab = this.app.activeTab.getFocusedTab()
|
||||
|
||||
if (focusedTab instanceof TerminalTabComponent && focusedTab.session) {
|
||||
options.cwd = await focusedTab.session.getWorkingDirectory()
|
||||
profile.options ??= {}
|
||||
profile.options.cwd = await focusedTab.session.getWorkingDirectory() ?? undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +66,7 @@ export class LocalProfilesService extends ProfileProvider {
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
inputs: {
|
||||
sessionOptions: options,
|
||||
profile,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -66,7 +84,7 @@ export class LocalProfilesService extends ProfileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
getDescription (profile: Profile): string {
|
||||
return profile.options?.command
|
||||
getDescription (profile: PartialProfile<LocalProfile>): string {
|
||||
return profile.options?.command ?? ''
|
||||
}
|
||||
}
|
||||
|
@@ -7,14 +7,14 @@ import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider<TerminalTabComponent> {
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:terminal-tab'
|
||||
return recoveryToken.type === 'app:local-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<TerminalTabComponent>> {
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
inputs: {
|
||||
sessionOptions: recoveryToken.sessionOptions,
|
||||
profile: recoveryToken.profile,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
@@ -23,9 +23,12 @@ export class RecoveryProvider extends TabRecoveryProvider<TerminalTabComponent>
|
||||
duplicate (recoveryToken: RecoveryToken): RecoveryToken {
|
||||
return {
|
||||
...recoveryToken,
|
||||
sessionOptions: {
|
||||
...recoveryToken.sessionOptions,
|
||||
restoreFromPTYID: null,
|
||||
profile: {
|
||||
...recoveryToken.profile,
|
||||
options: {
|
||||
...recoveryToken.profile.options,
|
||||
restoreFromPTYID: null,
|
||||
},
|
||||
},
|
||||
savedState: null,
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ export class DockMenuService {
|
||||
iconPath: process.execPath,
|
||||
iconIndex: 0,
|
||||
})),
|
||||
}] : null as any)
|
||||
}] : null)
|
||||
}
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
this.electron.app.dock.setMenu(this.electron.Menu.buildFromTemplate(
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService, ConfigService, AppService, ProfilesService } from 'tabby-core'
|
||||
import { Logger, LogService, ConfigService, ProfilesService, PartialProfile } from 'tabby-core'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
import { SessionOptions, LocalProfile } from '../api'
|
||||
import { LocalProfile } from '../api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TerminalService {
|
||||
@@ -10,7 +10,6 @@ export class TerminalService {
|
||||
|
||||
/** @hidden */
|
||||
private constructor (
|
||||
private app: AppService,
|
||||
private profilesService: ProfilesService,
|
||||
private config: ConfigService,
|
||||
log: LogService,
|
||||
@@ -18,52 +17,43 @@ export class TerminalService {
|
||||
this.logger = log.create('terminal')
|
||||
}
|
||||
|
||||
async getDefaultProfile (): Promise<LocalProfile> {
|
||||
async getDefaultProfile (): Promise<PartialProfile<LocalProfile>> {
|
||||
const profiles = await this.profilesService.getProfiles()
|
||||
let profile = profiles.find(x => x.id === this.config.store.terminal.profile)
|
||||
if (!profile) {
|
||||
profile = profiles.filter(x => x.type === 'local' && x.isBuiltin)[0]
|
||||
}
|
||||
return profile as LocalProfile
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a new terminal with a specific shell and CWD
|
||||
* @param pause Wait for a keypress when the shell exits
|
||||
*/
|
||||
async openTab (profile?: LocalProfile|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> {
|
||||
async openTab (profile?: PartialProfile<LocalProfile>|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> {
|
||||
if (!profile) {
|
||||
profile = await this.getDefaultProfile()
|
||||
}
|
||||
|
||||
cwd = cwd ?? profile.options.cwd
|
||||
const fullProfile = this.profilesService.getConfigProxyForProfile(profile)
|
||||
|
||||
cwd = cwd ?? fullProfile.options.cwd
|
||||
|
||||
if (cwd && !fs.existsSync(cwd)) {
|
||||
console.warn('Ignoring non-existent CWD:', cwd)
|
||||
cwd = null
|
||||
}
|
||||
|
||||
this.logger.info(`Starting profile ${profile.name}`, profile)
|
||||
this.logger.info(`Starting profile ${fullProfile.name}`, fullProfile)
|
||||
const options = {
|
||||
...profile.options,
|
||||
...fullProfile.options,
|
||||
pauseAfterExit: pause,
|
||||
cwd: cwd ?? undefined,
|
||||
}
|
||||
|
||||
return (await this.profilesService.openNewTabForProfile({
|
||||
...profile,
|
||||
...fullProfile,
|
||||
options,
|
||||
})) as TerminalTabComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a terminal with custom session options
|
||||
*/
|
||||
openTabWithOptions (sessionOptions: SessionOptions): TerminalTabComponent {
|
||||
this.logger.info('Using session options:', sessionOptions)
|
||||
return this.app.openNewTab({
|
||||
type: TerminalTabComponent,
|
||||
inputs: { sessionOptions },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import * as psNode from 'ps-node'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as os from 'os'
|
||||
import { Injector } from '@angular/core'
|
||||
import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core'
|
||||
import { BaseSession } from 'tabby-terminal'
|
||||
@@ -19,8 +18,6 @@ try {
|
||||
} catch { }
|
||||
|
||||
const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi
|
||||
const OSC1337Prefix = Buffer.from('\x1b]1337;')
|
||||
const OSC1337Suffix = Buffer.from('\x07')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class PTYProxy {
|
||||
@@ -90,7 +87,6 @@ export class Session extends BaseSession {
|
||||
private ptyClosed = false
|
||||
private pauseAfterExit = false
|
||||
private guessedCWD: string|null = null
|
||||
private reportedCWD: string
|
||||
private initialCWD: string|null = null
|
||||
private config: ConfigService
|
||||
private hostApp: HostAppService
|
||||
@@ -104,8 +100,6 @@ export class Session extends BaseSession {
|
||||
}
|
||||
|
||||
start (options: SessionOptions): void {
|
||||
this.name = options.name ?? ''
|
||||
|
||||
let pty: PTYProxy|null = null
|
||||
|
||||
if (options.restoreFromPTYID) {
|
||||
@@ -162,7 +156,7 @@ export class Session extends BaseSession {
|
||||
cwd,
|
||||
env: env,
|
||||
// `1` instead of `true` forces ConPTY even if unstable
|
||||
useConpty: (isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY ? 1 : false) as any,
|
||||
useConpty: isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY ? 1 : false,
|
||||
})
|
||||
|
||||
this.guessedCWD = cwd ?? null
|
||||
@@ -186,9 +180,7 @@ export class Session extends BaseSession {
|
||||
|
||||
this.pty.subscribe('data', (array: Uint8Array) => {
|
||||
this.pty!.ackData(array.length)
|
||||
|
||||
let data = Buffer.from(array)
|
||||
data = this.processOSC1337(data)
|
||||
const data = Buffer.from(array)
|
||||
this.emitOutput(data)
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
this.guessWindowsCWD(data.toString())
|
||||
@@ -295,7 +287,7 @@ export class Session extends BaseSession {
|
||||
}
|
||||
|
||||
supportsWorkingDirectory (): boolean {
|
||||
return !!(this.truePID || this.reportedCWD || this.guessedCWD)
|
||||
return !!(this.truePID ?? this.reportedCWD ?? this.guessedCWD)
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string|null> {
|
||||
@@ -338,22 +330,4 @@ export class Session extends BaseSession {
|
||||
this.guessedCWD = match[0]
|
||||
}
|
||||
}
|
||||
|
||||
private processOSC1337 (data: Buffer) {
|
||||
if (data.includes(OSC1337Prefix)) {
|
||||
const preData = data.subarray(0, data.indexOf(OSC1337Prefix))
|
||||
const params = data.subarray(data.indexOf(OSC1337Prefix) + OSC1337Prefix.length)
|
||||
const postData = params.subarray(params.indexOf(OSC1337Suffix) + OSC1337Suffix.length)
|
||||
const paramString = params.subarray(0, params.indexOf(OSC1337Suffix)).toString()
|
||||
|
||||
if (paramString.startsWith('CurrentDir=')) {
|
||||
this.reportedCWD = paramString.split('=')[1]
|
||||
if (this.reportedCWD.startsWith('~')) {
|
||||
this.reportedCWD = os.homedir() + this.reportedCWD.substring(1)
|
||||
}
|
||||
data = Buffer.concat([preData, postData])
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export class POSIXShellsProvider extends ShellProvider {
|
||||
.filter(x => x && !x.startsWith('#'))
|
||||
.map(x => ({
|
||||
id: slugify(x),
|
||||
name: x.split('/')[2],
|
||||
name: x.split('/').pop(),
|
||||
icon: 'fas fa-terminal',
|
||||
command: x,
|
||||
args: ['-l'],
|
||||
|
@@ -27,14 +27,14 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
|
||||
click: async () => {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = 'New profile name'
|
||||
const name = (await modal.result)?.name
|
||||
const name = (await modal.result)?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
const profile = {
|
||||
options: {
|
||||
...tab.sessionOptions,
|
||||
cwd: await tab.session?.getWorkingDirectory() ?? tab.sessionOptions.cwd,
|
||||
...tab.profile.options,
|
||||
cwd: await tab.session?.getWorkingDirectory() ?? tab.profile.options.cwd,
|
||||
},
|
||||
name,
|
||||
type: 'local',
|
||||
@@ -74,7 +74,11 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
{
|
||||
label: 'New terminal',
|
||||
click: () => {
|
||||
this.terminalService.openTabWithOptions((tab as any).sessionOptions)
|
||||
if (tab instanceof TerminalTabComponent) {
|
||||
this.profilesService.openNewTabForProfile(tab.profile)
|
||||
} else {
|
||||
this.terminalService.openTab()
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -98,9 +102,12 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
submenu: profiles.map(profile => ({
|
||||
label: profile.name,
|
||||
click: () => {
|
||||
this.terminalService.openTabWithOptions({
|
||||
...profile.options,
|
||||
runAsAdministrator: true,
|
||||
this.profilesService.openNewTabForProfile({
|
||||
...profile,
|
||||
options: {
|
||||
...profile.options,
|
||||
runAsAdministrator: true,
|
||||
},
|
||||
})
|
||||
},
|
||||
})),
|
||||
@@ -111,9 +118,12 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
items.push({
|
||||
label: 'Duplicate as administrator',
|
||||
click: () => {
|
||||
this.terminalService.openTabWithOptions({
|
||||
...tab.sessionOptions,
|
||||
runAsAdministrator: true,
|
||||
this.profilesService.openNewTabForProfile({
|
||||
...tab.profile,
|
||||
options: {
|
||||
...tab.profile.options,
|
||||
runAsAdministrator: true,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -128,13 +138,6 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
})
|
||||
}
|
||||
|
||||
if (tab instanceof TerminalTabComponent && tab.session?.supportsWorkingDirectory()) {
|
||||
items.push({
|
||||
label: 'Copy current path',
|
||||
click: () => tab.copyCurrentPath(),
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
@@ -7,11 +7,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
|
||||
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
|
||||
|
||||
"@types/shell-escape@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2"
|
||||
integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g==
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
@@ -357,11 +352,6 @@ runes@^0.4.2:
|
||||
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355"
|
||||
integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==
|
||||
|
||||
shell-escape@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133"
|
||||
integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=
|
||||
|
||||
side-channel@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||
|
1
tabby-plugin-manager/.gitignore
vendored
1
tabby-plugin-manager/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-plugin-manager",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Tabby's plugin manager",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -18,7 +18,6 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/semver": "^7.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"semver": "^7.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
.d-flex
|
||||
h3.mb-1 Installed
|
||||
button.btn.btn-outline-secondary.btn-sm.ml-auto((click)='openPluginsFolder()')
|
||||
button.btn.btn-secondary.btn-sm.ml-auto((click)='openPluginsFolder()')
|
||||
i.fas.fa-folder
|
||||
span Plugins folder
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import { PluginManagerService } from '../services/pluginManager.service'
|
||||
|
||||
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
||||
|
||||
const FORCE_ENABLE = ['tabby-core', 'tabby-settings']
|
||||
const FORCE_ENABLE = ['tabby-core', 'tabby-settings', 'tabby-electron', 'tabby-web']
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
|
@@ -7,18 +7,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3"
|
||||
integrity sha512-4g1jrL98mdOIwSOUh6LTlB0Cs9I0dQPwINUhBg7C6pN4HLr8GS8xsksJxilW6S6dQHVi2K/o+lQuQcg7LroCnw==
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||
dependencies:
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
|
0
tabby-serial/.gitignore
vendored
0
tabby-serial/.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-serial",
|
||||
"version": "1.0.145-nightly.0",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"description": "Serial connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -18,7 +18,8 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "14.14.14",
|
||||
"ansi-colors": "^4.1.1"
|
||||
"ansi-colors": "^4.1.1",
|
||||
"serialport-binding-webserialapi": "^1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^9.1.9",
|
||||
|
@@ -4,6 +4,7 @@ import { LogService, NotificationsService, Profile } from 'tabby-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Injector, NgZone } from '@angular/core'
|
||||
import { BaseSession, LoginScriptsOptions, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
||||
import { SerialService } from './services/serial.service'
|
||||
|
||||
export interface SerialProfile extends Profile {
|
||||
options: SerialProfileOptions
|
||||
@@ -19,7 +20,6 @@ export interface SerialProfileOptions extends StreamProcessingOptions, LoginScri
|
||||
xon?: boolean
|
||||
xoff?: boolean
|
||||
xany?: boolean
|
||||
color?: string
|
||||
}
|
||||
|
||||
export const BAUD_RATES = [
|
||||
@@ -39,9 +39,12 @@ export class SerialSession extends BaseSession {
|
||||
private streamProcessor: TerminalStreamProcessor
|
||||
private zone: NgZone
|
||||
private notifications: NotificationsService
|
||||
private serialService: SerialService
|
||||
|
||||
constructor (injector: Injector, public profile: SerialProfile) {
|
||||
super(injector.get(LogService).create(`serial-${profile.options.port}`))
|
||||
this.serialService = injector.get(SerialService)
|
||||
|
||||
this.zone = injector.get(NgZone)
|
||||
this.notifications = injector.get(NotificationsService)
|
||||
|
||||
@@ -58,6 +61,10 @@ export class SerialSession extends BaseSession {
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
if (!this.profile.options.port) {
|
||||
this.profile.options.port = (await this.serialService.listPorts())[0].name
|
||||
}
|
||||
|
||||
this.serial = new SerialPort(this.profile.options.port, {
|
||||
autoOpen: false,
|
||||
baudRate: parseInt(this.profile.options.baudrate as any),
|
||||
|
@@ -3,7 +3,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
a(ngbNavLink) General
|
||||
ng-template(ngbNavContent)
|
||||
.row
|
||||
.col-6
|
||||
.col-6(ng:if='hostApp.platform !== Platform.Web')
|
||||
.form-group
|
||||
label Device
|
||||
input.form-control(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs'
|
||||
import { ProfileSettingsComponent } from 'tabby-core'
|
||||
import { HostAppService, Platform, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { SerialPortInfo, BAUD_RATES, SerialProfile } from '../api'
|
||||
import { SerialService } from '../services/serial.service'
|
||||
|
||||
@@ -9,12 +9,14 @@ import { SerialService } from '../services/serial.service'
|
||||
@Component({
|
||||
template: require('./serialProfileSettings.component.pug'),
|
||||
})
|
||||
export class SerialProfileSettingsComponent implements ProfileSettingsComponent {
|
||||
export class SerialProfileSettingsComponent implements ProfileSettingsComponent<SerialProfile> {
|
||||
profile: SerialProfile
|
||||
foundPorts: SerialPortInfo[]
|
||||
Platform = Platform
|
||||
|
||||
constructor (
|
||||
private serial: SerialService,
|
||||
public hostApp: HostAppService,
|
||||
) { }
|
||||
|
||||
portsAutocomplete = text$ => text$.pipe(map(() => {
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
.mr-auto
|
||||
|
||||
button.btn.btn-sm.btn-link.mr-3((click)='changeBaudRate()', *ngIf='session && session.open')
|
||||
button.btn.btn-sm.btn-link.mr-3((click)='changeBaudRate()', *ngIf='session && session.open && hostApp.platform !== Platform.Web')
|
||||
span Change baud rate
|
||||
|
||||
button.btn.btn-sm.btn-link((click)='reconnect()', *ngIf='!session || !session.open')
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user