mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-29 21:54:37 +00:00
Compare commits
451 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a87d8871ad | ||
![]() |
d6fa3b02a9 | ||
![]() |
8a514fff17 | ||
![]() |
00b43e88dc | ||
![]() |
7048c2c10c | ||
![]() |
3f64789c55 | ||
![]() |
38e7f7f1b6 | ||
![]() |
b337bc5cfd | ||
![]() |
4ac5daed8c | ||
![]() |
82b724174c | ||
![]() |
e7ab2bcffd | ||
![]() |
3518c74508 | ||
![]() |
a9515d38d1 | ||
![]() |
e32bce29e0 | ||
![]() |
48ca696b0b | ||
![]() |
7bd9a887a6 | ||
![]() |
7bc66e9382 | ||
![]() |
b7ac65fce0 | ||
![]() |
601fff454d | ||
![]() |
730084425e | ||
![]() |
646094f210 | ||
![]() |
d5c9e1e9f6 | ||
![]() |
8d0bcb94b1 | ||
![]() |
f681f0e50a | ||
![]() |
2185f59111 | ||
![]() |
c5ba0b1e42 | ||
![]() |
b522284834 | ||
![]() |
3e2adfb0b8 | ||
![]() |
1a6f6759b9 | ||
![]() |
43a67bcd9d | ||
![]() |
5f7621cd8c | ||
![]() |
5e30a0b250 | ||
![]() |
8f02072762 | ||
![]() |
79fdba44c6 | ||
![]() |
efe0bad650 | ||
![]() |
a244362935 | ||
![]() |
924f5f13b1 | ||
![]() |
8f8dbd2023 | ||
![]() |
952d31a7d5 | ||
![]() |
f6e90a3121 | ||
![]() |
2818af7402 | ||
![]() |
9a36bdb9d1 | ||
![]() |
033468b0b0 | ||
![]() |
e6d83c6c58 | ||
![]() |
49d58c69bc | ||
![]() |
67ff355ca3 | ||
![]() |
5b0a7b39b7 | ||
![]() |
4ff5dea346 | ||
![]() |
4f244a126c | ||
![]() |
1926dffb7b | ||
![]() |
8642725b9f | ||
![]() |
7252f80573 | ||
![]() |
2d331332a4 | ||
![]() |
f36e2551b5 | ||
![]() |
bcf09c59e3 | ||
![]() |
b84c41d668 | ||
![]() |
2fbbb18bd9 | ||
![]() |
d48c2ddb36 | ||
![]() |
2b55e72be2 | ||
![]() |
b21fcf8f2b | ||
![]() |
02a99e8118 | ||
![]() |
fa66c96d60 | ||
![]() |
888c8217ca | ||
![]() |
de3aab4996 | ||
![]() |
8d587d27e5 | ||
![]() |
fa7bb79122 | ||
![]() |
f3255a7f31 | ||
![]() |
69680cbb8d | ||
![]() |
69c693a0a3 | ||
![]() |
69cdd5fb4a | ||
![]() |
59101cfcb3 | ||
![]() |
7120e32c91 | ||
![]() |
0a0d94ec91 | ||
![]() |
43e3277c0d | ||
![]() |
da28596968 | ||
![]() |
c1c7654380 | ||
![]() |
20116d7af6 | ||
![]() |
8c32fe010c | ||
![]() |
0a3debb691 | ||
![]() |
a3e5d04aac | ||
![]() |
153c98c03a | ||
![]() |
90fa980b70 | ||
![]() |
42bfde0e7f | ||
![]() |
14122bcfa2 | ||
![]() |
84879af11a | ||
![]() |
1c371347e9 | ||
![]() |
0c08a4d10c | ||
![]() |
3a66aaf9d7 | ||
![]() |
1992d99556 | ||
![]() |
60046da4b3 | ||
![]() |
e33e954e41 | ||
![]() |
f5e5091b10 | ||
![]() |
38691b80f2 | ||
![]() |
8d5eef6fa7 | ||
![]() |
e0f935519a | ||
![]() |
0f6855d978 | ||
![]() |
1b9081ce80 | ||
![]() |
e1098d9502 | ||
![]() |
3102f39706 | ||
![]() |
46ac0a6caf | ||
![]() |
68e1db040a | ||
![]() |
e34772b8b8 | ||
![]() |
14a4acdd92 | ||
![]() |
cf6558ec6a | ||
![]() |
f5f88d3d9d | ||
![]() |
64955bfcd6 | ||
![]() |
9fa9021a81 | ||
![]() |
43183401b7 | ||
![]() |
880b9ce82b | ||
![]() |
3584af524b | ||
![]() |
af174933d6 | ||
![]() |
c4490717c0 | ||
![]() |
70b6be7301 | ||
![]() |
8d5b0fe863 | ||
![]() |
03045eb952 | ||
![]() |
f7b0272be5 | ||
![]() |
4fa16c8a20 | ||
![]() |
855a7bbe14 | ||
![]() |
2b3694f517 | ||
![]() |
101177a865 | ||
![]() |
8b33f98c79 | ||
![]() |
98e52f50a9 | ||
![]() |
7551201796 | ||
![]() |
3fe2dccb94 | ||
![]() |
f53eb31274 | ||
![]() |
81663f351a | ||
![]() |
bf5d037cff | ||
![]() |
53d9af3279 | ||
![]() |
b7dd354313 | ||
![]() |
d8bc9ce859 | ||
![]() |
1bb9358f77 | ||
![]() |
fa77ff3995 | ||
![]() |
1ae8d9c643 | ||
![]() |
a560f0c96e | ||
![]() |
434bacf185 | ||
![]() |
79de7ec015 | ||
![]() |
dfdb3b051b | ||
![]() |
9fbf9136fc | ||
![]() |
25fdba7104 | ||
![]() |
c91707e94f | ||
![]() |
d665eef430 | ||
![]() |
4579e839cd | ||
![]() |
6e952180ec | ||
![]() |
a947254ca8 | ||
![]() |
1eb4a7fc26 | ||
![]() |
78f25a7679 | ||
![]() |
0c4d8b0784 | ||
![]() |
e2c8093b97 | ||
![]() |
c497a71361 | ||
![]() |
ec2982b1c4 | ||
![]() |
be0aeefdb3 | ||
![]() |
eadd8d563e | ||
![]() |
08f1ad4c75 | ||
![]() |
426606ba06 | ||
![]() |
7b59ba4b73 | ||
![]() |
0471fcec15 | ||
![]() |
4110d09dab | ||
![]() |
533837f5b7 | ||
![]() |
144924e579 | ||
![]() |
6902ccdb95 | ||
![]() |
7ed5aff168 | ||
![]() |
3f0db97a68 | ||
![]() |
231594d709 | ||
![]() |
e4ae114c71 | ||
![]() |
20000d16f8 | ||
![]() |
5e0a9b2e52 | ||
![]() |
fa70447223 | ||
![]() |
acf418b52f | ||
![]() |
28b84e38ca | ||
![]() |
3c4a078fa5 | ||
![]() |
52f4e88420 | ||
![]() |
16d9045a80 | ||
![]() |
07d7d8daba | ||
![]() |
b2b9476298 | ||
![]() |
cf7f3dffe3 | ||
![]() |
621005eb27 | ||
![]() |
d46e1de8aa | ||
![]() |
c44f3c5f25 | ||
![]() |
b3f9d48609 | ||
![]() |
edd7e9c7b7 | ||
![]() |
ab8061ab39 | ||
![]() |
c1a1f53707 | ||
![]() |
04097a0ef5 | ||
![]() |
85be974e64 | ||
![]() |
0df5fb4a34 | ||
![]() |
920b2b85b3 | ||
![]() |
4e4788bf57 | ||
![]() |
9aa60a9d0d | ||
![]() |
451ac51520 | ||
![]() |
04084aef33 | ||
![]() |
4198ca3fae | ||
![]() |
3b09dfa145 | ||
![]() |
923b559857 | ||
![]() |
58682b6bf1 | ||
![]() |
88c4198145 | ||
![]() |
a6c535414f | ||
![]() |
6ebb7723ff | ||
![]() |
07dd6600dc | ||
![]() |
cc6cfec907 | ||
![]() |
4ecfcfda36 | ||
![]() |
c5681b1376 | ||
![]() |
1fc57018e3 | ||
![]() |
8b8bacdf69 | ||
![]() |
3aaa419f8b | ||
![]() |
94819019ec | ||
![]() |
7b37035f75 | ||
![]() |
a5ef3507c3 | ||
![]() |
b9c6d30678 | ||
![]() |
009556f984 | ||
![]() |
87007d5ae3 | ||
![]() |
61ea2c77c8 | ||
![]() |
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 | ||
![]() |
8bfc1dc302 | ||
![]() |
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 | ||
![]() |
4e13a601cc | ||
![]() |
a159890cba | ||
![]() |
4534eefc1d | ||
![]() |
e06c44f973 | ||
![]() |
c1616b1a7a | ||
![]() |
5b34fa9371 | ||
![]() |
956a923ea2 | ||
![]() |
224abcb2c4 | ||
![]() |
e8af224f7b | ||
![]() |
a1d39563c3 | ||
![]() |
621536a078 | ||
![]() |
3cf9353d08 | ||
![]() |
6a2fa7efa9 | ||
![]() |
6b0fe0e2d1 | ||
![]() |
923876dc23 | ||
![]() |
233ae9cbe6 | ||
![]() |
09daf8102d | ||
![]() |
fd7893e9f8 | ||
![]() |
cf5c3e71f6 | ||
![]() |
5c30bbb7e4 | ||
![]() |
afce339187 | ||
![]() |
d528d1148b | ||
![]() |
786b31e2a2 | ||
![]() |
0ad32fa79d | ||
![]() |
93a89e3c86 | ||
![]() |
4e667edf9f | ||
![]() |
403bafe0a2 | ||
![]() |
40209dc60d | ||
![]() |
1ccd1df6e1 | ||
![]() |
800c1fa039 | ||
![]() |
0a67987e3c | ||
![]() |
6c7a8092a4 | ||
![]() |
9f87886a9b | ||
![]() |
0aa5df421d | ||
![]() |
d3498a6a46 | ||
![]() |
41a53a3e8e | ||
![]() |
636942ff86 | ||
![]() |
d1b874c191 | ||
![]() |
89f369abe6 | ||
![]() |
f7cea92900 | ||
![]() |
9a611709d0 | ||
![]() |
2094409d23 | ||
![]() |
2b061d3f77 | ||
![]() |
ed06c78a16 | ||
![]() |
36a6af05c4 | ||
![]() |
64b4eed9c3 | ||
![]() |
afc9270846 | ||
![]() |
2ea5edc101 | ||
![]() |
869a7e866c | ||
![]() |
1bd3b5301b | ||
![]() |
4ff66a39d8 | ||
![]() |
a3857d5dc8 | ||
![]() |
bf762cc4c7 | ||
![]() |
461cd2bec7 | ||
![]() |
da599567f8 | ||
![]() |
7f921c4d34 | ||
![]() |
0eb006f297 | ||
![]() |
07095f3476 | ||
![]() |
59b283067b | ||
![]() |
3635eee77d | ||
![]() |
64df798dc1 | ||
![]() |
f091206e37 | ||
![]() |
3148927395 | ||
![]() |
fb39bfd560 | ||
![]() |
f8cd9fcea7 | ||
![]() |
f1148cc47f | ||
![]() |
ffcc0d549a | ||
![]() |
861dd8ef86 | ||
![]() |
1e5e46eae1 | ||
![]() |
d644eec56e | ||
![]() |
a656699afd | ||
![]() |
93db8fa046 | ||
![]() |
05669046e9 | ||
![]() |
025d2d1748 | ||
![]() |
e17ba8c351 | ||
![]() |
d3dee44475 | ||
![]() |
0f0699d46a | ||
![]() |
b7540e59a8 | ||
![]() |
827345d899 | ||
![]() |
59de67ca58 | ||
![]() |
ccad826ca9 | ||
![]() |
838acc0a23 | ||
![]() |
804ae44ec8 | ||
![]() |
5f04c3b74b | ||
![]() |
4afd49e38c | ||
![]() |
ba32c69001 | ||
![]() |
e9a3947488 | ||
![]() |
b2f4b44123 | ||
![]() |
f4eacc1d66 | ||
![]() |
9155104662 | ||
![]() |
cbbd38ca83 | ||
![]() |
e5cf72e79b | ||
![]() |
a71d9c0727 | ||
![]() |
26e2c60265 | ||
![]() |
a5a0546e68 | ||
![]() |
92b34fbc08 | ||
![]() |
38b7e44f64 | ||
![]() |
3aa957a3f5 |
@@ -406,6 +406,51 @@
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "logicmachine123",
|
||||
"name": "Logic Machine",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/63876444?v=4",
|
||||
"profile": "https://git.io/JnP49",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "al-wi",
|
||||
"name": "Alexander Wiedemann",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11092199?v=4",
|
||||
"profile": "https://github.com/al-wi",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BoYeonJang",
|
||||
"name": "장보연",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59506394?v=4",
|
||||
"profile": "https://www.notion.so/3d45c6bd2cbd4f938873a4bd12e23375",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@@ -36,7 +36,9 @@ rules:
|
||||
'@typescript-eslint/prefer-readonly': off
|
||||
'@typescript-eslint/require-await': off
|
||||
'@typescript-eslint/strict-boolean-expressions': off
|
||||
'@typescript-eslint/no-misused-promises': off
|
||||
'@typescript-eslint/no-misused-promises':
|
||||
- error
|
||||
- checksVoidReturn: false
|
||||
'@typescript-eslint/typedef': off
|
||||
'@typescript-eslint/consistent-type-imports': off
|
||||
'@typescript-eslint/sort-type-union-intersection-members': off
|
||||
@@ -95,7 +97,9 @@ rules:
|
||||
- error
|
||||
- single
|
||||
- allowTemplateLiterals: true
|
||||
'@typescript-eslint/no-confusing-void-expression': off
|
||||
'@typescript-eslint/no-confusing-void-expression':
|
||||
- error
|
||||
- ignoreArrowShorthand: true
|
||||
'@typescript-eslint/no-non-null-assertion': off
|
||||
'@typescript-eslint/no-unnecessary-condition':
|
||||
- error
|
||||
@@ -116,3 +120,9 @@ rules:
|
||||
'@typescript-eslint/no-var-requires': off
|
||||
'@typescript-eslint/no-unsafe-argument': off
|
||||
'@typescript-eslint/restrict-plus-operands': off
|
||||
'@typescript-eslint/space-infix-ops': off
|
||||
'@typescript-eslint/no-type-alias':
|
||||
- error
|
||||
- allowAliases: in-unions-and-intersections
|
||||
allowLiterals: always
|
||||
allowCallbacks: always
|
||||
|
309
.github/workflows/build.yml
vendored
Normal file
309
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
name: Package-Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
Lint:
|
||||
runs-on: macos-11.0
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
npm i -g yarn@1.19.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
|
||||
- name: Build typings
|
||||
run: yarn run build:typings
|
||||
|
||||
- name: Lint
|
||||
run: yarn run lint
|
||||
|
||||
macOS-Build:
|
||||
runs-on: macos-11.0
|
||||
needs: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo npm i -g yarn@1.22.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
|
||||
- name: Build native deps
|
||||
run: scripts/build-native.js
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
|
||||
- name: Build native deps
|
||||
run: |
|
||||
rm -rf app/node_modules/cpu-features
|
||||
rm -rf app/node_modules/ssh2/crypto/build
|
||||
if: ${{ matrix.arch == 'arm64' }}
|
||||
|
||||
- name: Webpack
|
||||
run: yarn run build
|
||||
|
||||
- name: Prepackage plugins
|
||||
run: scripts/prepackage-plugins.js
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
|
||||
- run: sed -i '' 's/updateInfo = await/\/\/updateInfo = await/g' node_modules/app-builder-lib/out/targets/ArchiveTarget.js
|
||||
|
||||
- name: Build and sign packages
|
||||
run: scripts/build-macos.js
|
||||
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags'))
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
APPSTORE_USERNAME: ${{ secrets.APPSTORE_USERNAME }}
|
||||
APPSTORE_PASSWORD: ${{ secrets.APPSTORE_PASSWORD }}
|
||||
USE_HARD_LINKS: false
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build packages without signing
|
||||
run: scripts/build-macos.js
|
||||
if: "! (github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')))"
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
sudo npm install -g @sentry/cli --unsafe-perm
|
||||
./scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-pkg
|
||||
mv dist/*.pkg artifact-pkg/
|
||||
mkdir artifact-zip
|
||||
mv dist/*.zip artifact-zip/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload PKG
|
||||
with:
|
||||
name: macOS .pkg (${{matrix.arch}})
|
||||
path: artifact-pkg
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload ZIP
|
||||
with:
|
||||
name: macOS .zip (${{matrix.arch}})
|
||||
path: artifact-zip
|
||||
|
||||
Linux-Build:
|
||||
runs-on: ubuntu-18.04
|
||||
needs: Lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install bsdtar zsh
|
||||
npm i -g yarn@1.19.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
npm run patch
|
||||
|
||||
- name: Build native deps
|
||||
run: scripts/build-native.js
|
||||
|
||||
- name: Webpack
|
||||
run: yarn run build
|
||||
|
||||
- name: Prepackage plugins
|
||||
run: scripts/prepackage-plugins.js
|
||||
|
||||
- name: Build packages
|
||||
run: scripts/build-linux.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
USE_HARD_LINKS: false
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build web resources
|
||||
run: zsh -c 'tar czf tabby-web.tar.gz (tabby-*|web)/dist'
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
sudo npm install -g @sentry/cli --unsafe-perm
|
||||
./scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Upload packages to packagecloud.io
|
||||
uses: TykTechnologies/packagecloud-action@main
|
||||
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
with:
|
||||
repo: 'eugeny/tabby'
|
||||
dir: 'dist'
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-deb
|
||||
mv dist/*.deb artifact-deb/ || true
|
||||
mkdir artifact-rpm
|
||||
mv dist/*.rpm artifact-rpm/ || true
|
||||
mkdir artifact-pacman
|
||||
mv dist/*.pacman artifact-pacman/ || true
|
||||
mkdir artifact-snap
|
||||
mv dist/*.snap artifact-snap/ || true
|
||||
mkdir artifact-tar.gz
|
||||
mv dist/*.tar.gz artifact-tar.gz/ || true
|
||||
mkdir artifact-web
|
||||
mv tabby-web.tar.gz artifact-web/ || true
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload DEB
|
||||
with:
|
||||
name: Linux DEB
|
||||
path: artifact-deb
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload RPM
|
||||
with:
|
||||
name: Linux RPM
|
||||
path: artifact-rpm
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Pacman Package
|
||||
with:
|
||||
name: Linux Pacman
|
||||
path: artifact-pacman
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Snap
|
||||
with:
|
||||
name: Linux Snap
|
||||
path: artifact-snap
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Linux tarball
|
||||
with:
|
||||
name: Linux tarball
|
||||
path: artifact-tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload web tarball
|
||||
with:
|
||||
name: Web tarball
|
||||
path: artifact-web
|
||||
|
||||
Windows-Build:
|
||||
runs-on: windows-2016
|
||||
needs: Lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Build
|
||||
shell: powershell
|
||||
run: |
|
||||
npm i -g yarn@1.19.1
|
||||
yarn
|
||||
node scripts/build-native.js
|
||||
yarn run build
|
||||
node scripts/prepackage-plugins.js
|
||||
|
||||
- name: Build and sign packages
|
||||
run: node scripts/build-windows.js
|
||||
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags'))
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build packages without signing
|
||||
run: node scripts/build-windows.js
|
||||
if: "!(github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')))"
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
npm install @sentry/cli
|
||||
node scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-setup
|
||||
mv dist/*-setup.exe artifact-setup/
|
||||
mkdir artifact-portable
|
||||
mv dist/*-portable.zip artifact-portable/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload installer
|
||||
with:
|
||||
name: Windows installer
|
||||
path: artifact-setup
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload portable build
|
||||
with:
|
||||
name: Windows portable build
|
||||
path: artifact-portable
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
|
31
.github/workflows/lint.yml
vendored
31
.github/workflows/lint.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
npm i -g yarn@1.19.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
|
||||
- name: Build typings
|
||||
run: yarn run build:typings
|
||||
|
||||
- name: Lint
|
||||
run: yarn run lint
|
107
.github/workflows/linux.yml
vendored
107
.github/workflows/linux.yml
vendored
@@ -1,107 +0,0 @@
|
||||
name: Linux Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install bsdtar zsh
|
||||
npm i -g yarn@1.19.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
npm run patch
|
||||
|
||||
- name: Build native deps
|
||||
run: scripts/build-native.js
|
||||
|
||||
- name: Webpack
|
||||
run: yarn run build
|
||||
|
||||
- name: Prepackage plugins
|
||||
run: scripts/prepackage-plugins.js
|
||||
|
||||
- name: Build packages
|
||||
run: scripts/build-linux.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
USE_HARD_LINKS: false
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build web resources
|
||||
run: zsh -c 'tar czf tabby-web.tar.gz (tabby-*|web)/dist'
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
sudo npm install -g @sentry/cli --unsafe-perm
|
||||
./scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-deb
|
||||
mv dist/*.deb artifact-deb/ || true
|
||||
mkdir artifact-rpm
|
||||
mv dist/*.rpm artifact-rpm/ || true
|
||||
mkdir artifact-pacman
|
||||
mv dist/*.pacman artifact-pacman/ || true
|
||||
mkdir artifact-snap
|
||||
mv dist/*.snap artifact-snap/ || true
|
||||
mkdir artifact-tar.gz
|
||||
mv dist/*.tar.gz artifact-tar.gz/ || true
|
||||
mkdir artifact-web
|
||||
mv tabby-web.tar.gz artifact-web/ || true
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload DEB
|
||||
with:
|
||||
name: Linux DEB
|
||||
path: artifact-deb
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload RPM
|
||||
with:
|
||||
name: Linux RPM
|
||||
path: artifact-rpm
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Pacman Package
|
||||
with:
|
||||
name: Linux Pacman
|
||||
path: artifact-pacman
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Snap
|
||||
with:
|
||||
name: Linux Snap
|
||||
path: artifact-snap
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload Linux tarball
|
||||
with:
|
||||
name: Linux tarball
|
||||
path: artifact-tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload web tarball
|
||||
with:
|
||||
name: Web tarball
|
||||
path: artifact-web
|
99
.github/workflows/macos.yml
vendored
99
.github/workflows/macos.yml
vendored
@@ -1,99 +0,0 @@
|
||||
name: macOS Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-11.0
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo npm i -g yarn@1.22.1
|
||||
cd app
|
||||
yarn
|
||||
cd ..
|
||||
rm app/node_modules/.yarn-integrity
|
||||
yarn
|
||||
|
||||
- name: Build native deps
|
||||
run: scripts/build-native.js
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
|
||||
- name: Build native deps
|
||||
run: |
|
||||
rm -rf app/node_modules/cpu-features
|
||||
rm -rf app/node_modules/ssh2/crypto/build
|
||||
if: ${{ matrix.arch == 'arm64' }}
|
||||
|
||||
- name: Webpack
|
||||
run: yarn run build
|
||||
|
||||
- name: Prepackage plugins
|
||||
run: scripts/prepackage-plugins.js
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
|
||||
- run: sed -i '' 's/updateInfo = await/\/\/updateInfo = await/g' node_modules/app-builder-lib/out/targets/ArchiveTarget.js
|
||||
|
||||
- name: Build and sign packages
|
||||
run: scripts/build-macos.js
|
||||
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags'))
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
APPSTORE_USERNAME: ${{ secrets.APPSTORE_USERNAME }}
|
||||
APPSTORE_PASSWORD: ${{ secrets.APPSTORE_PASSWORD }}
|
||||
USE_HARD_LINKS: false
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build packages without signing
|
||||
run: scripts/build-macos.js
|
||||
if: "! (github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')))"
|
||||
env:
|
||||
ARCH: ${{matrix.arch}}
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
sudo npm install -g @sentry/cli --unsafe-perm
|
||||
./scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-pkg
|
||||
mv dist/*.pkg artifact-pkg/
|
||||
mkdir artifact-zip
|
||||
mv dist/*.zip artifact-zip/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload PKG
|
||||
with:
|
||||
name: macOS .pkg (${{matrix.arch}})
|
||||
path: artifact-pkg
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload ZIP
|
||||
with:
|
||||
name: macOS .zip (${{matrix.arch}})
|
||||
path: artifact-zip
|
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
|
66
.github/workflows/windows.yml
vendored
66
.github/workflows/windows.yml
vendored
@@ -1,66 +0,0 @@
|
||||
name: Windows Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2016
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.1.5
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: Build
|
||||
shell: powershell
|
||||
run: |
|
||||
npm i -g yarn@1.19.1
|
||||
yarn
|
||||
node scripts/build-native.js
|
||||
yarn run build
|
||||
node scripts/prepackage-plugins.js
|
||||
|
||||
- name: Build and sign packages
|
||||
run: node scripts/build-windows.js
|
||||
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags'))
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
# DEBUG: electron-builder,electron-builder:*
|
||||
|
||||
- name: Build packages without signing
|
||||
run: node scripts/build-windows.js
|
||||
if: "!(github.repository == 'Eugeny/tabby' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')))"
|
||||
|
||||
- name: Upload symbols
|
||||
run: |
|
||||
npm install @sentry/cli
|
||||
node scripts/sentry-upload.js
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-setup
|
||||
mv dist/*-setup.exe artifact-setup/
|
||||
mkdir artifact-portable
|
||||
mv dist/*-portable.zip artifact-portable/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload installer
|
||||
with:
|
||||
name: Installer
|
||||
path: artifact-setup
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload portable build
|
||||
with:
|
||||
name: Portable build
|
||||
path: artifact-portable
|
@@ -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
|
||||
```
|
||||
@@ -145,4 +145,6 @@ export default class MyModule { }
|
||||
|
||||
See `tabby-core/src/api.ts`, `tabby-settings/src/api.ts`, `tabby-local/src/api.ts` and `tabby-terminal/src/api.ts` for the available extension points.
|
||||
|
||||
Also check out [the example plugin](https://github.com/Eugeny/tabby-clippy).
|
||||
|
||||
Publish your plugin on NPM with a `tabby-plugin` keyword to make it appear in the Plugin Manager.
|
||||
|
215
README.ko-KR.md
Normal file
215
README.ko-KR.md
Normal file
@@ -0,0 +1,215 @@
|
||||
[](https://tabby.sh)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Eugeny/tabby/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/tabby/total.svg?label=DOWNLOADS&logo=github&style=for-the-badge"></a> <a href="https://nightly.link/Eugeny/tabby/workflows/build/master"><img src="https://shields.io/badge/-Nightly%20Builds-orange?logo=hackthebox&logoColor=fff&style=for-the-badge"/></a> <a href="https://matrix.to/#/#tabby-general:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/tabby-general:matrix.org?logo=matrix&style=for-the-badge&color=magenta"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ko-fi.com/J3J8KWTF">
|
||||
<img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=2" width="150">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
### 다운로드:
|
||||
|
||||
* [Latest release](https://github.com/Eugeny/tabby/releases/latest)
|
||||
* [Repositories](https://packagecloud.io/eugeny/tabby): [Debian/Ubuntu-based](https://packagecloud.io/eugeny/tabby/install#bash-deb), [RPM-based](https://packagecloud.io/eugeny/tabby/install#bash-rpm)
|
||||
* [Latest nightly build](https://nightly.link/Eugeny/tabby/workflows/build/master)
|
||||
|
||||
----
|
||||
|
||||
**Tabby** (구 **Terminus**)는 Windows, macOS 및 Linux용으로 뛰어난 구성의 터미널 에뮬레이터, SSH 및 시리얼 클라이언트입니다.
|
||||
|
||||
* 통합 SSH 클라이언트 및 연결 관리자
|
||||
* 통합 시리얼 터미널
|
||||
* 테마 및 색 구성표
|
||||
* 전체 구성이 가능한 단축키 및 다중 코드 단축키
|
||||
* 창 분할
|
||||
* 이전 탭 사용을 기억
|
||||
* PowerShell (및 PS Core), WSL, Git-Bash, Cygwin, Cmder 및 CMD 지원
|
||||
* Zmodem을 통한 SSH 세션 간의 직접 파일 전송
|
||||
* 2바이트 문자를 포함한 전체 유니코드 지원
|
||||
* 빠르게 출력되는 것에 대해 휩쓸리지 않음
|
||||
* 탭 완성을 포함한 Windows에서의 적절한 셸 환경 (Clink을 통해)
|
||||
* SSH 시크릿 및 구성을 위한 통합 암호화 컨테이너
|
||||
|
||||
# 목차 <!-- omit in toc -->
|
||||
|
||||
- [Tabby는 무엇인가](#about)
|
||||
- [터미널 기능](#terminal)
|
||||
- [SSH 클라이언트](#ssh)
|
||||
- [시리얼 터미널](#serial)
|
||||
- [포터블](#portable)
|
||||
- [플러그인](#plugins)
|
||||
- [테마](#themes)
|
||||
- [기여](#contributing)
|
||||
|
||||
<a name="about"></a>
|
||||
|
||||
# Tabby는 무엇인가
|
||||
|
||||
* **Tabby는** Windows의 표준 터미널 (conhost), PowerShell ISE, PuTTY 또는 iTerm의 대안 프로그램입니다.
|
||||
|
||||
* **Tabby는** 새로운 셸이나 MinGW 또는 Cygwin을 대체하지 **않습니다**. 가볍지도 않습니다. - RAM 사용량이 중요한 경우, [Conemu](https://conemu.github.io) 또는 [Alacritty](https://github.com/jwilm/alacritty)를 고려하십시오.
|
||||
|
||||
<a name="terminal"></a>
|
||||
|
||||
# 터미널 기능
|
||||
|
||||

|
||||
|
||||
* A V220 터미널 + 다양한 확장
|
||||
* 여러 개의 분할 창 중첩
|
||||
* 모든 측면에 탭이 위치함
|
||||
* 전역 스폰 단축키가 있는 도킹 가능한 윈도우 ("Quake console")
|
||||
* 진행률 탐지
|
||||
* 프로세스 완료 시 알림
|
||||
* 괄호 붙여넣기, 여러 줄 붙여넣기 경고
|
||||
* 폰트 합자(ligatures)
|
||||
* 커스텀 셸 프로필
|
||||
* RMB 붙여넣기 및 복사 선택 옵션 (PuTTY 스타일)
|
||||
|
||||
<a name="ssh"></a>
|
||||
# SSH 클라이언트
|
||||
|
||||

|
||||
|
||||
* 연결 관리자가 있는 SSH2 클라이언트
|
||||
* X11 및 포트 포워딩
|
||||
* 자동 jump 호스트 관리
|
||||
* 에이전트 전달 (Pageant 및 Windows 기본 OpenSSH 에이전트 포함)
|
||||
* 로그인 스크립트
|
||||
|
||||
<a name="serial"></a>
|
||||
# 시리얼 터미널
|
||||
|
||||
* 연결 저장
|
||||
* Readline 입력 지원
|
||||
* 선택적 hex byte별 입력 및 hexdump 출력
|
||||
* 개행 변환
|
||||
* 자동 재접속
|
||||
|
||||
<a name="portable"></a>
|
||||
# 포터블
|
||||
|
||||
`Tabby.exe`가 있는 동일한 위치에 `data` 폴더를 생성하면 Windows에서 Tabby가 포터블 앱으로 실행됩니다.
|
||||
|
||||
<a name="plugins"></a>
|
||||
# 플러그인
|
||||
|
||||
플러그인과 테마는 Tabby 내부의 설정에서 직접 설치할 수 있습니다.
|
||||
|
||||
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - m터미널의 경로 및 URL을 클릭 가능하게
|
||||
* [docker](https://github.com/Eugeny/tabby-docker) - Docker 컨테이너에 연결
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - 접두사, 접미사 및/또는 문자열 제거를 제공하여 터미널 탭의 제목을 수정
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 하나 또는 모든 터미널 탭에 신속한 명령 전송
|
||||
* [save-output](https://github.com/Eugeny/tabby-save-output) - 터미널 출력을 파일에 기록
|
||||
* [sync-config](https://github.com/starxg/terminus-sync-config) - 구성을 Gist 또는 Gitee에 동기화
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - 항상 당신을 귀찮게 하는 예제 플러그인
|
||||
|
||||
<a name="themes"></a>
|
||||
# 테마
|
||||
|
||||
* [hype](https://github.com/Eugeny/tabby-theme-hype) - Hyper에서 영감을 받은 테마
|
||||
* [relaxed](https://github.com/Relaxed-Theme/relaxed-terminal-themes#terminus) - Tabby를 위해 여유로움을 제공하는 테마
|
||||
* [gruvbox](https://github.com/porkloin/terminus-theme-gruvbox)
|
||||
* [windows10](https://www.npmjs.com/package/terminus-theme-windows10)
|
||||
* [altair](https://github.com/yxuko/terminus-altair)
|
||||
|
||||
# 스폰서 <!-- omit in toc -->
|
||||
|
||||
[](https://packagecloud.io)
|
||||
|
||||
[**packagecloud**](https://packagecloud.io)가 무료 Debian/RPM 저장소 호스팅을 제공하였습니다.
|
||||
|
||||
<a name="contributing"></a>
|
||||
# 기여
|
||||
|
||||
Pull requests and plugins are welcome!
|
||||
|
||||
프로젝트 배치 방법에 대한 자세한 내용과 매우 간단한 플러그인 개발 튜토리얼은 [HACKING.md](https://github.com/Eugeny/tabby/blob/master/HACKING.md) 및 [API docs](http://ajenti.org/terminus-docs/)를 참조하십시오.
|
||||
|
||||
---
|
||||
<a name="contributors"></a>
|
||||
|
||||
여기있는 멋진 사람들에게 진심으로 감사합니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="http://www.russellmyers.com"><img src="https://avatars2.githubusercontent.com/u/184085?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Russell Myers</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=mezner" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.morwire.com"><img src="https://avatars1.githubusercontent.com/u/3991658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Austin Warren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ehwarren" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Drachenkaetzchen"><img src="https://avatars1.githubusercontent.com/u/162974?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Felicia Hummel</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=Drachenkaetzchen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mikemaccana"><img src="https://avatars2.githubusercontent.com/u/172594?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mike MacCana</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=mikemaccana" title="Tests">⚠️</a> <a href="#design-mikemaccana" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/yxuko"><img src="https://avatars1.githubusercontent.com/u/1786317?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yacine Kanzari</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yxuko" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/BBJip"><img src="https://avatars2.githubusercontent.com/u/32908927?v=4?s=100" width="100px;" alt=""/><br /><sub><b>BBJip</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=BBJip" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Futagirl"><img src="https://avatars2.githubusercontent.com/u/33533958?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Futagirl</b></sub></a><br /><a href="#design-Futagirl" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://www.levrik.io"><img src="https://avatars3.githubusercontent.com/u/9491603?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Levin Rickert</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=levrik" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://kwonoj.github.io"><img src="https://avatars2.githubusercontent.com/u/1210596?v=4?s=100" width="100px;" alt=""/><br /><sub><b>OJ Kwon</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kwonoj" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Domain"><img src="https://avatars2.githubusercontent.com/u/903197?v=4?s=100" width="100px;" alt=""/><br /><sub><b>domain</b></sub></a><br /><a href="#plugin-Domain" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/tabby/commits?author=Domain" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.jbrumond.me"><img src="https://avatars1.githubusercontent.com/u/195127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James Brumond</b></sub></a><br /><a href="#plugin-kbjr" title="Plugin/utility libraries">🔌</a></td>
|
||||
<td align="center"><a href="http://www.growingwiththeweb.com"><img src="https://avatars0.githubusercontent.com/u/2193314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Imms</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=Tyriar" title="Code">💻</a> <a href="#plugin-Tyriar" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/tabby/commits?author=Tyriar" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/baflo"><img src="https://avatars2.githubusercontent.com/u/834350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Florian Bachmann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=baflo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://michael-kuehnel.de"><img src="https://avatars2.githubusercontent.com/u/441011?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Kühnel</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=mischah" title="Code">💻</a> <a href="#design-mischah" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/NieLeben"><img src="https://avatars3.githubusercontent.com/u/47182955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tilmann Meyer</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=NieLeben" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.jubeat.net"><img src="https://avatars3.githubusercontent.com/u/11289158?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PM Extra</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/issues?q=author%3APMExtra" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://jjuhas.keybase.pub//"><img src="https://avatars1.githubusercontent.com/u/6438760?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=IgnusG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://hans-koch.me"><img src="https://avatars0.githubusercontent.com/u/1093709?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Hans Koch</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=hammster" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://thepuzzlemaker.info"><img src="https://avatars3.githubusercontent.com/u/12666617?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dak Smyth</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ThePuzzlemaker" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://yfwz100.github.io"><img src="https://avatars2.githubusercontent.com/u/983211?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Wang Zhi</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yfwz100" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jack1142"><img src="https://avatars0.githubusercontent.com/u/6032823?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jack1142</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=jack1142" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hdougie"><img src="https://avatars1.githubusercontent.com/u/450799?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Howie Douglas</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=hdougie" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://chriskaczor.com"><img src="https://avatars2.githubusercontent.com/u/180906?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chris Kaczor</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ckaczor" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.boxmein.net"><img src="https://avatars1.githubusercontent.com/u/358714?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Johannes Kadak</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=boxmein" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/LeSeulArtichaut"><img src="https://avatars1.githubusercontent.com/u/38361244?v=4?s=100" width="100px;" alt=""/><br /><sub><b>LeSeulArtichaut</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=LeSeulArtichaut" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/CyrilTaylor"><img src="https://avatars0.githubusercontent.com/u/12631466?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cyril Taylor</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=CyrilTaylor" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstefanou"><img src="https://avatars3.githubusercontent.com/u/51129173?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nstefanou</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nstefanou" title="Code">💻</a> <a href="#plugin-nstefanou" title="Plugin/utility libraries">🔌</a></td>
|
||||
<td align="center"><a href="https://github.com/orin220444"><img src="https://avatars3.githubusercontent.com/u/30747229?v=4?s=100" width="100px;" alt=""/><br /><sub><b>orin220444</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=orin220444" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Goobles"><img src="https://avatars3.githubusercontent.com/u/8776771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gobius Dolhain</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=Goobles" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/3l0w"><img src="https://avatars2.githubusercontent.com/u/37798980?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gwilherm Folliot</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=3l0w" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Dimitory"><img src="https://avatars0.githubusercontent.com/u/475955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dmitry Pronin</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=dimitory" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/JonathanBeverley"><img src="https://avatars1.githubusercontent.com/u/20328966?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Beverley</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=JonathanBeverley" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/zend"><img src="https://avatars1.githubusercontent.com/u/25160?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zenghai Liang</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zend" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://about.me/matishadow"><img src="https://avatars0.githubusercontent.com/u/9083085?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mateusz Tracz</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=matishadow" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://zergpool.com"><img src="https://avatars3.githubusercontent.com/u/36234677?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pinpin</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=pinpins" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/TakuroOnoda"><img src="https://avatars0.githubusercontent.com/u/1407926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Takuro Onoda</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=TakuroOnoda" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/frauhottelmann"><img src="https://avatars2.githubusercontent.com/u/902705?v=4?s=100" width="100px;" alt=""/><br /><sub><b>frauhottelmann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=frauhottelmann" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://patalong.pl"><img src="https://avatars.githubusercontent.com/u/29167842?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Piotr Patalong</b></sub></a><br /><a href="#design-VectorKappa" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/clarkwang"><img src="https://avatars.githubusercontent.com/u/157076?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Clark Wang</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=clarkwang" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iamchating"><img src="https://avatars.githubusercontent.com/u/7088153?v=4?s=100" width="100px;" alt=""/><br /><sub><b>iamchating</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=iamchating" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/starxg"><img src="https://avatars.githubusercontent.com/u/34997494?v=4?s=100" width="100px;" alt=""/><br /><sub><b>starxg</b></sub></a><br /><a href="#plugin-starxg" title="Plugin/utility libraries">🔌</a></td>
|
||||
<td align="center"><a href="http://hashnote.net/"><img src="https://avatars.githubusercontent.com/u/546312?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alisue</b></sub></a><br /><a href="#design-lambdalisue" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
이 프로젝트는 [모든 기여자](https://github.com/all-contributors/all-contributors)의 규격을 따릅니다. 어떠한 종류의 기여도 모두 환영합니다!
|
||||
|
||||
<img src="https://ga-beacon.appspot.com/UA-3278102-18/github/readme" width="1"/>
|
70
README.md
70
README.md
@@ -1,38 +1,48 @@
|
||||

|
||||
[](https://tabby.sh)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Eugeny/tabby/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/tabby/total.svg?label=RELEASE&logo=github&style=for-the-badge"></a> <a href="https://nightly.link/Eugeny/tabby/workflows/windows/master"><img src="https://shields.io/badge/-Nightly-blue?logo=windows&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/tabby/workflows/macos/master"><img src="https://shields.io/badge/-Nightly-black?logo=apple&style=for-the-badge"/></a> <a href="https://nightly.link/Eugeny/tabby/workflows/linux/master"><img src="https://shields.io/badge/-Nightly-orange?logo=linux&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=magenta&logo=gitter&style=for-the-badge"></a>
|
||||
<a href="https://github.com/Eugeny/tabby/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/tabby/total.svg?label=DOWNLOADS&logo=github&style=for-the-badge"></a> <a href="https://nightly.link/Eugeny/tabby/workflows/build/master"><img src="https://shields.io/badge/-Nightly%20Builds-orange?logo=hackthebox&logoColor=fff&style=for-the-badge"/></a> <a href="https://matrix.to/#/#tabby-general:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/tabby-general:matrix.org?logo=matrix&style=for-the-badge&color=magenta"></a>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
||||
----
|
||||
|
||||
**Tabby** (formerly **Terminus**) is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
||||
### Downloads:
|
||||
|
||||
* Integrated SSH client and connection manager
|
||||
* [Latest release](https://github.com/Eugeny/tabby/releases/latest)
|
||||
* [Repositories](https://packagecloud.io/eugeny/tabby): [Debian/Ubuntu-based](https://packagecloud.io/eugeny/tabby/install#bash-deb), [RPM-based](https://packagecloud.io/eugeny/tabby/install#bash-rpm)
|
||||
* [Latest nightly build](https://nightly.link/Eugeny/tabby/workflows/build/master)
|
||||
|
||||
<br/>
|
||||
<p align="center">
|
||||
This README is also available in: <a href="./README.ko-KR.md">Korean</a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
[**Tabby**](https://tabby.sh) (formerly **Terminus**) is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
||||
|
||||
* Integrated SSH and Telnet client and connection manager
|
||||
* Integrated serial terminal
|
||||
* Theming and color schemes
|
||||
* Fully configurable shortcuts and multi-chord shortcuts
|
||||
* Split panes
|
||||
* Remembers your tabs
|
||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, MSYS2, Cmder and CMD support
|
||||
* Direct file transfer from/to SSH sessions via Zmodem
|
||||
* Full Unicode support including double-width characters
|
||||
* Doesn't choke on fast-flowing outputs
|
||||
* Proper shell experience on Windows including tab completion (via Clink)
|
||||
* Integrated encrypted container for SSH secrets and configuration
|
||||
|
||||
---
|
||||
# Contents <!-- omit in toc -->
|
||||
|
||||
# Contents
|
||||
|
||||
- [Contents](#contents)
|
||||
- [What Tabby is and isn't](#what-tabby-is-and-isnt)
|
||||
- [Terminal features](#terminal-features)
|
||||
- [SSH Client](#ssh-client)
|
||||
@@ -43,13 +53,15 @@
|
||||
- [Contributing](#contributing)
|
||||
|
||||
<a name="about"></a>
|
||||
|
||||
# What Tabby is and isn't
|
||||
|
||||
* **Tabby is** an alternative to Windows' standard terminal (conhost), PowerShell ISE, PuTTY or iTerm
|
||||
* **Tabby is** an alternative to Windows' standard terminal (conhost), PowerShell ISE, PuTTY, macOS Terminal.app and iTerm
|
||||
|
||||
* **Tabby is not** a new shell or a MinGW or Cygwin replacement. Neither is it lightweight - if RAM usage is of importance, consider [Conemu](https://conemu.github.io) or [Alacritty](https://github.com/jwilm/alacritty)
|
||||
|
||||
<a name="terminal"></a>
|
||||
|
||||
# Terminal features
|
||||
|
||||

|
||||
@@ -95,21 +107,28 @@ Tabby will run as a portable app on Windows, if you create a `data` folder in th
|
||||
|
||||
Plugins and themes can be installed directly from the Settings view inside Tabby.
|
||||
|
||||
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
|
||||
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to hterm tabs
|
||||
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
|
||||
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - makes paths and URLs in the terminal clickable
|
||||
* [docker](https://github.com/Eugeny/tabby-docker) - connect to Docker containers
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||
* [save-output](https://github.com/Eugeny/tabby-save-output) - record terminal output into a file
|
||||
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - an example plugin which annoys you all the time
|
||||
|
||||
<a name="themes"></a>
|
||||
# Themes
|
||||
|
||||
* [hype](https://github.com/Eugeny/terminus-theme-hype) - a Hyper inspired theme
|
||||
* [relaxed](https://github.com/Relaxed-Theme/relaxed-terminal-themes#terminus) - the Relaxed theme for Tabby
|
||||
* [gruvbox](https://github.com/porkloin/terminus-theme-gruvbox)
|
||||
* [windows10](https://www.npmjs.com/package/terminus-theme-windows10)
|
||||
* [altair](https://github.com/yxuko/terminus-altair)
|
||||
* [hype](https://github.com/Eugeny/tabby-theme-hype) - a Hyper inspired theme
|
||||
* [relaxed](https://github.com/Relaxed-Theme/relaxed-terminal-themes#terminus) - the Relaxed theme for Tabby
|
||||
* [gruvbox](https://github.com/porkloin/terminus-theme-gruvbox)
|
||||
* [windows10](https://www.npmjs.com/package/terminus-theme-windows10)
|
||||
* [altair](https://github.com/yxuko/terminus-altair)
|
||||
|
||||
# Sponsors <!-- omit in toc -->
|
||||
|
||||
[](https://packagecloud.io)
|
||||
|
||||
[**packagecloud**](https://packagecloud.io) has provided free Debian/RPM repository hosting
|
||||
|
||||
<a name="contributing"></a>
|
||||
# Contributing
|
||||
@@ -184,6 +203,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<tr>
|
||||
<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>
|
||||
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.notion.so/3d45c6bd2cbd4f938873a4bd12e23375"><img src="https://avatars.githubusercontent.com/u/59506394?v=4?s=100" width="100px;" alt=""/><br /><sub><b>장보연</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=BoYeonJang" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -193,3 +217,5 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
<img src="https://ga-beacon.appspot.com/UA-3278102-18/github/readme" width="1"/>
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron'
|
||||
import * as promiseIpc from 'electron-promise-ipc'
|
||||
import * as remote from '@electron/remote/main'
|
||||
import { exec } from 'mz/child_process'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { Subject, throttleTime } from 'rxjs'
|
||||
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
@@ -19,6 +21,8 @@ export class Application {
|
||||
private tray?: Tray
|
||||
private ptyManager = new PTYManager()
|
||||
private windows: Window[] = []
|
||||
private globalHotkey$ = new Subject<void>()
|
||||
private quitRequested = false
|
||||
userPluginsPath: string
|
||||
|
||||
constructor () {
|
||||
@@ -33,12 +37,14 @@ export class Application {
|
||||
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
|
||||
globalShortcut.unregisterAll()
|
||||
for (const spec of specs) {
|
||||
globalShortcut.register(spec, () => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
globalShortcut.register(spec, () => this.globalHotkey$.next())
|
||||
}
|
||||
})
|
||||
|
||||
this.globalHotkey$.pipe(throttleTime(100)).subscribe(() => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
|
||||
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
|
||||
return pluginManager.install(this.userPluginsPath, name, version)
|
||||
})
|
||||
@@ -47,6 +53,10 @@ export class Application {
|
||||
return pluginManager.uninstall(this.userPluginsPath, name)
|
||||
})
|
||||
|
||||
;(promiseIpc as any).on('get-default-mac-shell', async () => {
|
||||
return (await exec(`/usr/bin/dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString().split(' ')[1].trim()
|
||||
})
|
||||
|
||||
const configData = loadConfig()
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('no-sandbox')
|
||||
@@ -73,6 +83,12 @@ export class Application {
|
||||
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
|
||||
app.commandLine.appendSwitch(flag[0], flag[1])
|
||||
}
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (this.quitRequested || process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
init (): void {
|
||||
@@ -217,7 +233,8 @@ export class Application {
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Cmd+Q',
|
||||
click () {
|
||||
click: () => {
|
||||
this.quitRequested = true
|
||||
app.quit()
|
||||
},
|
||||
},
|
||||
|
@@ -16,12 +16,6 @@ export function parseArgs (argv: string[], cwd: string): any {
|
||||
.command('profile [profileName]', 'open a tab with specified profile', {
|
||||
profileName: { type: 'string' },
|
||||
})
|
||||
.command('connect-ssh [connectionName]', 'open a tab for a saved SSH connection', {
|
||||
connectionName: { type: 'string' },
|
||||
})
|
||||
.command('connect-serial [connectionName]', 'open a tab for a saved serial connection', {
|
||||
connectionName: { type: 'string' },
|
||||
})
|
||||
.command('paste [text]', 'paste stdin into the active tab', yargs => {
|
||||
return yargs.option('escape', {
|
||||
alias: 'e',
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'v8-compile-cache'
|
||||
import './portable'
|
||||
import 'source-map-support/register'
|
||||
import './sentry'
|
||||
@@ -25,10 +26,6 @@ app.on('activate', () => {
|
||||
}
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
process.on('uncaughtException' as any, err => {
|
||||
console.log(err)
|
||||
application.broadcast('uncaughtException', err)
|
||||
|
@@ -1,18 +1,27 @@
|
||||
import * as nodePTY from 'node-pty'
|
||||
import { StringDecoder } from './stringDecoder'
|
||||
import * as nodePTY from '@tabby-gang/node-pty'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ipcMain } from 'electron'
|
||||
import { Application } from './app'
|
||||
import { UTF8Splitter } from './utfSplitter'
|
||||
import { Subject, debounceTime } from 'rxjs'
|
||||
|
||||
class PTYDataQueue {
|
||||
private buffers: Buffer[] = []
|
||||
private delta = 0
|
||||
private maxChunk = 1024
|
||||
private maxDelta = 1024 * 50
|
||||
private maxChunk = 1024 * 100
|
||||
private maxDelta = this.maxChunk * 5
|
||||
private flowPaused = false
|
||||
private decoder = new StringDecoder()
|
||||
private decoder = new UTF8Splitter()
|
||||
private output$ = new Subject<Buffer>()
|
||||
|
||||
constructor (private pty: nodePTY.IPty, private onData: (data: Buffer) => void) { }
|
||||
constructor (private pty: nodePTY.IPty, private onData: (data: Buffer) => void) {
|
||||
this.output$.pipe(debounceTime(500)).subscribe(() => {
|
||||
const remainder = this.decoder.flush()
|
||||
if (remainder.length) {
|
||||
this.onData(remainder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
push (data: Buffer) {
|
||||
this.buffers.push(data)
|
||||
@@ -61,7 +70,9 @@ class PTYDataQueue {
|
||||
}
|
||||
|
||||
private emitData (data: Buffer) {
|
||||
this.onData(this.decoder.write(data))
|
||||
const validChunk = this.decoder.write(data)
|
||||
this.onData(validChunk)
|
||||
this.output$.next(validChunk)
|
||||
}
|
||||
|
||||
private pause () {
|
||||
|
32
app/lib/utfSplitter.ts
Normal file
32
app/lib/utfSplitter.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
const partials = [
|
||||
[0b110, 5, 0],
|
||||
[0b1110, 4, 1],
|
||||
[0b11110, 3, 2],
|
||||
]
|
||||
|
||||
export class UTF8Splitter {
|
||||
private internal = Buffer.alloc(0)
|
||||
|
||||
write (data: Buffer): Buffer {
|
||||
this.internal = Buffer.concat([this.internal, data])
|
||||
|
||||
let keep = 0
|
||||
for (const [pattern, shift, maxOffset] of partials) {
|
||||
for (let offset = 0; offset < maxOffset + 1; offset++) {
|
||||
if (this.internal[this.internal.length - offset - 1] >> shift === pattern) {
|
||||
keep = Math.max(keep, offset + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.internal.slice(0, this.internal.length - keep)
|
||||
this.internal = this.internal.slice(this.internal.length - keep)
|
||||
return result
|
||||
}
|
||||
|
||||
flush (): Buffer {
|
||||
const result = this.internal
|
||||
this.internal = Buffer.alloc(0)
|
||||
return result
|
||||
}
|
||||
}
|
@@ -1,8 +1,7 @@
|
||||
import * as glasstron from 'glasstron'
|
||||
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions } from 'electron'
|
||||
import { Subject, Observable, debounceTime } from 'rxjs'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage } from 'electron'
|
||||
import ElectronConfig = require('electron-config')
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -29,6 +28,8 @@ abstract class GlasstronWindow extends BrowserWindow {
|
||||
|
||||
const macOSVibrancyType = process.platform === 'darwin' ? compareVersions.compare(macOSRelease().version, '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null
|
||||
|
||||
const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`)
|
||||
|
||||
export class Window {
|
||||
ready: Promise<void>
|
||||
private visible = new Subject<boolean>()
|
||||
@@ -40,6 +41,7 @@ export class Window {
|
||||
private lastVibrancy: { enabled: boolean, type?: string } | null = null
|
||||
private disableVibrancyWhileDragging = false
|
||||
private configStore: any
|
||||
private touchBarControl: any
|
||||
|
||||
get visible$ (): Observable<boolean> { return this.visible }
|
||||
get closed$ (): Observable<void> { return this.closed }
|
||||
@@ -118,7 +120,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()
|
||||
}
|
||||
})
|
||||
@@ -128,7 +130,15 @@ export class Window {
|
||||
this.window.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
this.window.webContents.setZoomFactor(1)
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
if (process.platform === 'darwin') {
|
||||
this.touchBarControl = new TouchBar.TouchBarSegmentedControl({
|
||||
segments: [],
|
||||
change: index => this.send('touchbar-selection', index),
|
||||
})
|
||||
this.window.setTouchBar(new TouchBar({
|
||||
items: [this.touchBarControl],
|
||||
}))
|
||||
} else {
|
||||
this.window.setMenu(null)
|
||||
}
|
||||
|
||||
@@ -358,13 +368,21 @@ export class Window {
|
||||
this.window.close()
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-touch-bar', (_event, segments, selectedIndex) => {
|
||||
this.touchBarControl.segments = segments.map(s => ({
|
||||
label: s.label,
|
||||
icon: s.hasActivity ? activityIcon : undefined,
|
||||
}))
|
||||
this.touchBarControl.selectedIndex = selectedIndex
|
||||
})
|
||||
|
||||
this.window.webContents.on('new-window', event => event.preventDefault())
|
||||
|
||||
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
||||
this.disableVibrancyWhileDragging = value
|
||||
})
|
||||
|
||||
let moveEndedTimeout: number|null = null
|
||||
let moveEndedTimeout: any = null
|
||||
const onBoundsChange = () => {
|
||||
if (!this.lastVibrancy?.enabled || !this.disableVibrancyWhileDragging) {
|
||||
return
|
||||
|
@@ -14,45 +14,38 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^12.0.0",
|
||||
"@angular/common": "^12.0.0",
|
||||
"@angular/compiler": "^12.0.0",
|
||||
"@angular/core": "^12.0.0",
|
||||
"@angular/forms": "^12.0.0",
|
||||
"@angular/platform-browser": "^12.0.0",
|
||||
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||
"@angular/cdk": "^12.2.0",
|
||||
"@electron/remote": "1.2.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^9.1.1",
|
||||
"@tabby-gang/node-pty": "^0.11.0-beta.200",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-promise-ipc": "^2.2.4",
|
||||
"electron-updater": "^4.3.9",
|
||||
"fontmanager-redux": "1.1.0",
|
||||
"glasstron": "0.0.7",
|
||||
"js-yaml": "4.1.0",
|
||||
"keytar": "^7.7.0",
|
||||
"mz": "^2.7.0",
|
||||
"native-process-working-directory": "^1.0.2",
|
||||
"ngx-toastr": "^14.0.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"npm": "6",
|
||||
"rxjs": "^7.1.0",
|
||||
"yargs": "^17.0.1",
|
||||
"zone.js": "^0.11.4"
|
||||
"rxjs": "^7.2.0",
|
||||
"source-map-support": "^0.5.19",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"yargs": "^17.1.0"
|
||||
},
|
||||
"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.3",
|
||||
"@types/node": "15.12.5",
|
||||
"@types/mz": "2.7.4",
|
||||
"@types/node": "16.0.1",
|
||||
"ngx-filesize": "^2.0.16",
|
||||
"node-abi": "^2.30.0",
|
||||
"source-map-support": "^0.5.19"
|
||||
"node-abi": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tabby-community-color-schemes": "*",
|
||||
|
@@ -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'
|
||||
|
@@ -5,13 +5,15 @@ import 'rxjs'
|
||||
import './global.scss'
|
||||
import './toastr.scss'
|
||||
|
||||
// Importing before @angular/*
|
||||
import { findPlugins, initModuleLookup, loadPlugins } from './plugins'
|
||||
|
||||
import { enableProdMode, NgModuleRef, ApplicationRef } from '@angular/core'
|
||||
import { enableDebugTools } from '@angular/platform-browser'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
import { getRootModule } from './app.module'
|
||||
import { findPlugins, initModuleLookup, loadPlugins } from './plugins'
|
||||
import { BootstrapData, BOOTSTRAP_DATA, PluginInfo } from '../../tabby-core/src/api/mainProcess'
|
||||
|
||||
// Always land on the start view
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -158,3 +158,31 @@ ngb-typeahead-window {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@@ -18,28 +18,47 @@ function normalizePath (p: string): string {
|
||||
|
||||
const builtinPluginsPath = process.env.TABBY_DEV ? path.dirname(remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const cachedBuiltinModules = {
|
||||
'@angular/animations': require('@angular/animations'),
|
||||
'@angular/common': require('@angular/common'),
|
||||
'@angular/compiler': require('@angular/compiler'),
|
||||
'@angular/core': require('@angular/core'),
|
||||
'@angular/forms': require('@angular/forms'),
|
||||
'@angular/platform-browser': require('@angular/platform-browser'),
|
||||
'@angular/platform-browser/animations': require('@angular/platform-browser/animations'),
|
||||
'@angular/platform-browser-dynamic': require('@angular/platform-browser-dynamic'),
|
||||
'@ng-bootstrap/ng-bootstrap': require('@ng-bootstrap/ng-bootstrap'),
|
||||
'ngx-toastr': require('ngx-toastr'),
|
||||
rxjs: require('rxjs'),
|
||||
'rxjs/operators': require('rxjs/operators'),
|
||||
'zone.js/dist/zone.js': require('zone.js/dist/zone.js'),
|
||||
}
|
||||
|
||||
const builtinModules = [
|
||||
'@angular/animations',
|
||||
'@angular/common',
|
||||
'@angular/compiler',
|
||||
'@angular/core',
|
||||
'@angular/forms',
|
||||
'@angular/platform-browser',
|
||||
'@angular/platform-browser-dynamic',
|
||||
'@ng-bootstrap/ng-bootstrap',
|
||||
'ngx-toastr',
|
||||
'rxjs',
|
||||
'rxjs/operators',
|
||||
...Object.keys(cachedBuiltinModules),
|
||||
'tabby-core',
|
||||
'tabby-local',
|
||||
'tabby-settings',
|
||||
'tabby-terminal',
|
||||
'zone.js/dist/zone.js',
|
||||
]
|
||||
|
||||
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
const originalRequire = (global as any).require
|
||||
;(global as any).require = function (query: string) {
|
||||
if (cachedBuiltinModules[query]) {
|
||||
return cachedBuiltinModules[query]
|
||||
}
|
||||
return originalRequire.apply(this, [query])
|
||||
}
|
||||
|
||||
const cachedBuiltinModules = {}
|
||||
const originalModuleRequire = nodeModule.prototype.require
|
||||
nodeModule.prototype.require = function (query: string) {
|
||||
if (cachedBuiltinModules[query]) {
|
||||
return cachedBuiltinModules[query]
|
||||
}
|
||||
return originalModuleRequire.call(this, query)
|
||||
}
|
||||
|
||||
export type ProgressCallback = (current: number, total: number) => void
|
||||
|
||||
export function initModuleLookup (userPluginsPath: string): void {
|
||||
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
@@ -57,24 +76,10 @@ export function initModuleLookup (userPluginsPath: string): void {
|
||||
}
|
||||
|
||||
builtinModules.forEach(m => {
|
||||
cachedBuiltinModules[m] = nodeRequire(m)
|
||||
if (!cachedBuiltinModules[m]) {
|
||||
cachedBuiltinModules[m] = nodeRequire(m)
|
||||
}
|
||||
})
|
||||
|
||||
const originalRequire = (global as any).require
|
||||
;(global as any).require = function (query: string) {
|
||||
if (cachedBuiltinModules[query]) {
|
||||
return cachedBuiltinModules[query]
|
||||
}
|
||||
return originalRequire.apply(this, [query])
|
||||
}
|
||||
|
||||
const originalModuleRequire = nodeModule.prototype.require
|
||||
nodeModule.prototype.require = function (query: string) {
|
||||
if (cachedBuiltinModules[query]) {
|
||||
return cachedBuiltinModules[query]
|
||||
}
|
||||
return originalModuleRequire.call(this, query)
|
||||
}
|
||||
}
|
||||
|
||||
export async function findPlugins (): Promise<PluginInfo[]> {
|
||||
|
@@ -31,52 +31,29 @@ module.exports = {
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
loader: 'awesome-typescript-loader',
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFileName: path.resolve(__dirname, 'tsconfig.json'),
|
||||
configFile: path.resolve(__dirname, 'tsconfig.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||
{
|
||||
test: /\.(png|svg)$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 999999,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'fonts/[name].[ext]',
|
||||
},
|
||||
},
|
||||
test: /\.(png|svg|ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
type: 'asset',
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
'@angular/core': 'commonjs @angular/core',
|
||||
'@angular/compiler': 'commonjs @angular/compiler',
|
||||
'@angular/platform-browser': 'commonjs @angular/platform-browser',
|
||||
'@angular/platform-browser-dynamic': 'commonjs @angular/platform-browser-dynamic',
|
||||
'@angular/forms': 'commonjs @angular/forms',
|
||||
'@angular/common': 'commonjs @angular/common',
|
||||
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
|
||||
'@electron/remote': 'commonjs @electron/remote',
|
||||
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||
child_process: 'commonjs child_process',
|
||||
electron: 'commonjs electron',
|
||||
fs: 'commonjs fs',
|
||||
'ngx-toastr': 'commonjs ngx-toastr',
|
||||
module: 'commonjs module',
|
||||
mz: 'commonjs mz',
|
||||
path: 'commonjs path',
|
||||
rxjs: 'commonjs rxjs',
|
||||
'zone.js': 'commonjs zone.js/dist/zone.js',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
|
@@ -25,29 +25,28 @@ module.exports = {
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
loader: 'awesome-typescript-loader',
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFileName: path.resolve(__dirname, 'tsconfig.main.json'),
|
||||
configFile: path.resolve(__dirname, 'tsconfig.main.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||
'any-promise': 'commonjs any-promise',
|
||||
electron: 'commonjs electron',
|
||||
'electron-config': 'commonjs electron-config',
|
||||
'electron-debug': 'commonjs electron-debug',
|
||||
'electron-promise-ipc': 'commonjs electron-promise-ipc',
|
||||
'electron-vibrancy': 'commonjs electron-vibrancy',
|
||||
fs: 'commonjs fs',
|
||||
glasstron: 'commonjs glasstron',
|
||||
mz: 'commonjs mz',
|
||||
npm: 'commonjs npm',
|
||||
'node-pty': 'commonjs node-pty',
|
||||
'node:os': 'commonjs os',
|
||||
'@tabby-gang/node-pty': 'commonjs @tabby-gang/node-pty',
|
||||
path: 'commonjs path',
|
||||
rxjs: 'commonjs rxjs',
|
||||
'rxjs/operators': 'commonjs rxjs/operators',
|
||||
util: 'commonjs util',
|
||||
'source-map-support': 'commonjs source-map-support',
|
||||
'windows-swca': 'commonjs windows-swca',
|
||||
|
267
app/yarn.lock
267
app/yarn.lock
@@ -2,54 +2,14 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@angular/animations@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-12.0.0.tgz#5f845b1a58ffb6f3ea6103edf0756ac65320b725"
|
||||
integrity sha512-BG/Ksk3863I7GKUem73Kty4UeU289oN+iPo/0O0x2dJCzNcpafML0GJpz4lg/RT9l6UddFviI4q9NiopR+eJfw==
|
||||
"@angular/cdk@^12.2.0":
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.0.tgz#7c6de53522ef7cf911d86e187f3df2a90e8fee49"
|
||||
integrity sha512-Dts+KIMz6EdzQxaWBFcNwgWAHVPkI5pnOGMidKKVOmjezSUN6mhfBKq8emgsddJMRAqz/1VHMAEaRkp0VoBKiA==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/common@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-12.0.0.tgz#a4b992f3af997e9e957500148100f3f2a90ad3e9"
|
||||
integrity sha512-d6+WSnCFcxAHBsbCvBC3Rutmk+tB5CEdKhkTBY/vGe0A/MjbayzHR4IDv2i0+UZDLSgMJubqh3iCPUcSglXSEg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/compiler@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-12.0.0.tgz#bb0d4f464fee4803dbda49d862474f771c31f633"
|
||||
integrity sha512-7NdZNyxm9KLlRMmmtId6RfV6VbQIUMDxN44R+ax66BoWsuhdYXUDsDO554LwYwrjnnXXGkurDJhv7umeRwaZGw==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/core@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-12.0.0.tgz#d16a217f0919b3b161229118c52b1f703815eb71"
|
||||
integrity sha512-fwXtF6qP8pr07+El/dg67RmgsI4Ubfi+E5YLjYKQ62gM8MzYyYGmLPakFzFnbzYrOr05zdprrbcVgGtMRHapMA==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/forms@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-12.0.0.tgz#faf5e3e36a8c4f57f42a5b3dd11786f39c94d693"
|
||||
integrity sha512-/Z2AWd2k/9cs+WwXBlZ8yUqgGsHYcp8g6PUCehZQk1gd/4n4FOKvTIGiypajGUPwO4GOHJDzibfCsGw8MenCpQ==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/platform-browser-dynamic@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.0.0.tgz#295036e7b487b6dbe3b13db763a371675d391ee6"
|
||||
integrity sha512-Rkxr/KVOZGuGSuIYo2XZYbOpyS2t2jpLPS65KUUcOEwktj4hSv5VZ2soZF18tG5ZNbx06C1QDW/j9HwmZjEh5g==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@angular/platform-browser@^12.0.0":
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-12.0.0.tgz#097805ad9a5db044dc0a74c1294cdfa5122eca4c"
|
||||
integrity sha512-h+uMMluRh4dqJIor7EpvwNKRjv4xCxpttizJlqbo3vfcoOoLDoc9SvEFiXxd+UVh3S0re8zBsyBIJl+gTVFKWQ==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
tslib "^2.2.0"
|
||||
optionalDependencies:
|
||||
parse5 "^5.0.0"
|
||||
|
||||
"@electron/remote@1.2.0":
|
||||
version "1.2.0"
|
||||
@@ -65,13 +25,6 @@
|
||||
update-notifier "^2.2.0"
|
||||
yargs "^8.0.2"
|
||||
|
||||
"@ng-bootstrap/ng-bootstrap@^9.1.1":
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-9.1.1.tgz#5a629915ea93b4f9b4d61854cb6862d99a7c9ca4"
|
||||
integrity sha512-m31qKJylYueXm+a3YEoOfnrJYR1lovb7WgaQwvXQz3dDmtaYRX4n8aPeCMp1VrI7hFfFITKWo0GxPaI3JIFk4w==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@serialport/binding-abstract@^9.0.7":
|
||||
version "9.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@serialport/binding-abstract/-/binding-abstract-9.0.7.tgz#d2c7ecea0f100bdf20187bfc0d34ba90f5504e1e"
|
||||
@@ -143,17 +96,29 @@
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
|
||||
"@types/mz@2.7.3":
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.3.tgz#e42a21e73f5f9340fe4a176981fafb1eb8cc6c12"
|
||||
integrity sha512-Zp1NUJ4Alh3gaun0a5rkF3DL7b2j1WB6rPPI5h+CJ98sQnxe9qwskClvupz/4bqChGR3L/BRhTjlaOwR+uiZJg==
|
||||
"@tabby-gang/node-pty@^0.11.0-beta.200":
|
||||
version "0.11.0-beta.200"
|
||||
resolved "https://registry.yarnpkg.com/@tabby-gang/node-pty/-/node-pty-0.11.0-beta.200.tgz#485cd6d85a04f4b272b81a9862578d7fc38cdfb5"
|
||||
integrity sha512-32ANParjnd38SzvICaLYvEBlTZAE2sqsgEZPK6ITgd38FcCsS/yvvsDZcjkclbxApnMM2rJDaYjsZMa0lr9Iyg==
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
"@types/mz@2.7.4":
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
|
||||
integrity sha512-Zs0imXxyWT20j3Z2NwKpr0IO2LmLactBblNyLua5Az4UHuqOQ02V3jPTgyKwDkuc33/ahw+C3O1PIZdrhFMuQA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@15.12.5":
|
||||
version "15.12.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.5.tgz#9a78318a45d75c9523d2396131bd3cca54b2d185"
|
||||
integrity sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg==
|
||||
"@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==
|
||||
|
||||
"@types/semver@^7.3.5":
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59"
|
||||
integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==
|
||||
|
||||
JSONStream@^1.3.4, JSONStream@^1.3.5:
|
||||
version "1.3.5"
|
||||
@@ -399,6 +364,14 @@ buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@8.7.5:
|
||||
version "8.7.5"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz#fbe59e274818885e0d2e358d5b7017c34ae6b0f5"
|
||||
integrity sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
sax "^1.2.4"
|
||||
|
||||
builtins@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz"
|
||||
@@ -733,6 +706,13 @@ debug@^4.0.1, debug@^4.3.1:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debuglog@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz"
|
||||
@@ -892,6 +872,20 @@ electron-promise-ipc@^2.2.4:
|
||||
serialize-error "^5.0.0"
|
||||
uuid "^3.0.1"
|
||||
|
||||
electron-updater@^4.3.9:
|
||||
version "4.3.9"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.9.tgz#247c660bafad7c07935e1b81acd3e9a5fd733154"
|
||||
integrity sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==
|
||||
dependencies:
|
||||
"@types/semver" "^7.3.5"
|
||||
builder-util-runtime "8.7.5"
|
||||
fs-extra "^10.0.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.4"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isequal "^4.5.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
@@ -1116,6 +1110,15 @@ fs-constants@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
|
||||
integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-minipass@^1.2.5:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
|
||||
@@ -1280,6 +1283,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz"
|
||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz"
|
||||
@@ -1590,7 +1598,7 @@ isstream@~0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz"
|
||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||
|
||||
js-yaml@4.1.0:
|
||||
js-yaml@4.1.0, js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
@@ -1627,6 +1635,15 @@ json-stringify-safe@~5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonparse@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
@@ -1672,6 +1689,11 @@ lazy-property@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147"
|
||||
integrity sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=
|
||||
|
||||
lazy-val@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d"
|
||||
integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==
|
||||
|
||||
lcid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
|
||||
@@ -1878,6 +1900,16 @@ lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.escaperegexp@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
|
||||
|
||||
lodash.union@~4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
|
||||
@@ -1913,6 +1945,13 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
macos-native-processlist@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/macos-native-processlist/-/macos-native-processlist-2.0.0.tgz"
|
||||
@@ -2099,20 +2138,20 @@ ngx-filesize@^2.0.16:
|
||||
filesize ">= 4.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
ngx-toastr@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286"
|
||||
integrity sha512-dnDzSY73pF6FvNyxdh6ftfvXvUg6SU7MAT3orPUCzA77t3ZcFslro06zk4NCA2g67RF7dBwM0OJ/y0SN6fdGYw==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
node-abi@^2.20.0, node-abi@^2.30.0, node-abi@^2.7.0:
|
||||
node-abi@^2.20.0, node-abi@^2.7.0:
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.0.tgz#8be53bf3e7945a34eea10e0fc9a5982776cf550b"
|
||||
integrity sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-abi@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.0.0.tgz#aaeec41ffa8dd436de7a97345ff6f5c99eeafeec"
|
||||
integrity sha512-bAfE5Pp+qqHiz4GkpH64HqHUgK2DippKB3QuYbsOp8QoR8c7S646jJMfsOj+WHZO5dPssO3j+54LwG3w3HeYWg==
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
node-addon-api@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz"
|
||||
@@ -2149,13 +2188,6 @@ node-gyp@^5.0.2, node-gyp@^5.1.0:
|
||||
tar "^4.4.12"
|
||||
which "^1.3.1"
|
||||
|
||||
node-pty@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d"
|
||||
integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg==
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz"
|
||||
@@ -2621,6 +2653,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"
|
||||
@@ -2642,9 +2679,9 @@ path-key@^2.0.0:
|
||||
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz"
|
||||
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-type@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -3016,10 +3053,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rxjs@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.1.0.tgz#94202d27b19305ef7b1a4f330277b2065df7039e"
|
||||
integrity sha512-gCFO5iHIbRPwznl6hAYuwNFld8W4S2shtSJIqG27ReWXo9IWrCyEICxUA+6vJHwSR/OakoenC4QsDxq50tzYmw==
|
||||
rxjs@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.2.0.tgz#5cd12409639e9514a71c9f5f9192b2c4ae94de31"
|
||||
integrity sha512-aX8w9OpKrQmiPKfT1bqETtUr9JygIz6GZ+gql8v7CijClsP0laoFUdKzxFAoWuRdSlOdU2+crss+cMf+cqMTnw==
|
||||
dependencies:
|
||||
tslib "~2.1.0"
|
||||
|
||||
@@ -3038,6 +3075,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
semver-diff@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
|
||||
@@ -3050,6 +3092,13 @@ semver-diff@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
serialize-error@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac"
|
||||
@@ -3457,10 +3506,10 @@ tough-cookie@~2.5.0:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
tslib@^2.0.0, tslib@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -3520,6 +3569,11 @@ unique-string@^1.0.0:
|
||||
dependencies:
|
||||
crypto-random-string "^1.0.0"
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
@@ -3582,6 +3636,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"
|
||||
@@ -3649,12 +3708,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"
|
||||
@@ -3752,6 +3811,11 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yargs-parser@^15.0.1:
|
||||
version "15.0.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3"
|
||||
@@ -3789,10 +3853,10 @@ yargs@^14.2.3:
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^15.0.1"
|
||||
|
||||
yargs@^17.0.1:
|
||||
version "17.0.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
|
||||
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
|
||||
yargs@^17.1.0:
|
||||
version "17.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf"
|
||||
integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
@@ -3820,10 +3884,3 @@ yargs@^8.0.2:
|
||||
which-module "^2.0.0"
|
||||
y18n "^3.2.1"
|
||||
yargs-parser "^7.0.0"
|
||||
|
||||
zone.js@^0.11.4:
|
||||
version "0.11.4"
|
||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.4.tgz#0f70dcf6aba80f698af5735cbb257969396e8025"
|
||||
integrity sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
@@ -85,7 +85,6 @@ deb:
|
||||
- gnome-keyring
|
||||
- libnotify4
|
||||
- libsecret-1-0
|
||||
- libappindicator1
|
||||
- libxtst6
|
||||
- libnss3
|
||||
afterInstall: build/linux/after-install.tpl
|
||||
|
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,40 +1,53 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@sentry/cli": "^1.64.2",
|
||||
"@sentry/electron": "^2.5.0",
|
||||
"@terminus-term/to-string-loader": "1.1.7-beta.1",
|
||||
"@angular/animations": "^12.0.0",
|
||||
"@angular/common": "^12.0.0",
|
||||
"@angular/compiler": "^12.0.0",
|
||||
"@angular/core": "^12.0.0",
|
||||
"@angular/forms": "^12.0.0",
|
||||
"@angular/platform-browser": "^12.0.0",
|
||||
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||
"@sentry/cli": "^1.67.2",
|
||||
"@sentry/electron": "^2.5.2",
|
||||
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
||||
"@types/electron-config": "^3.2.2",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"deep-equal": "2.0.5",
|
||||
"@types/electron-debug": "^2.1.0",
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/js-yaml": "^4.0.1",
|
||||
"@types/node": "15.12.5",
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/js-yaml": "^4.0.2",
|
||||
"@types/node": "16.0.1",
|
||||
"@types/sortablejs": "^1.10.7",
|
||||
"@types/webpack-env": "^1.16.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"apply-loader": "2.0.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"axios": "^0.21.1",
|
||||
"clone-deep": "^4.0.1",
|
||||
"compare-versions": "^3.6.0",
|
||||
"core-js": "^3.14.0",
|
||||
"core-js": "^3.15.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "5.2.6",
|
||||
"electron": "13.1.4",
|
||||
"css-loader": "^6.2.0",
|
||||
"electron": "13.2.2",
|
||||
"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.29.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"electron-notarize": "^1.0.1",
|
||||
"electron-rebuild": "^3.2.3",
|
||||
"eslint": "^7.32.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"graceful-fs": "^4.2.8",
|
||||
"html-loader": "2.1.2",
|
||||
"json-loader": "0.5.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"macos-release": "^2.5.0",
|
||||
"node-abi": "^2.30.0",
|
||||
"macos-release": "^3.0.1",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-toastr": "^14.0.0",
|
||||
"node-abi": "^3.0.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",
|
||||
@@ -44,33 +57,40 @@
|
||||
"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.6.0",
|
||||
"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.4.0",
|
||||
"style-loader": "^3.2.1",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.21.2",
|
||||
"typescript": "^4.2.4",
|
||||
"url-loader": "^4.1.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"tslib": "^2.3.1",
|
||||
"typedoc": "^0.21.6",
|
||||
"typescript": "^4.3.5",
|
||||
"val-loader": "4.0.0",
|
||||
"webpack": "^5.41.0",
|
||||
"webpack": "^5.51.1",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"yaml-loader": "0.6.0"
|
||||
"webpack-cli": "^4.8.0",
|
||||
"yaml-loader": "0.6.0",
|
||||
"zone.js": "^0.11.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"lzma-native": "^8.0.0",
|
||||
"*/node-abi": "^2.30.0",
|
||||
"**/graceful-fs": "^4.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config tabby-core/webpack.config.js && webpack --color --config tabby-settings/webpack.config.js && webpack --color --config tabby-terminal/webpack.config.js && webpack --color --config tabby-local/webpack.config.js && webpack --color --config tabby-plugin-manager/webpack.config.js && webpack --color --config tabby-community-color-schemes/webpack.config.js && webpack --color --config tabby-ssh/webpack.config.js && webpack --color --config tabby-serial/webpack.config.js && webpack --color --config tabby-electron/webpack.config.js && webpack --color --config tabby-web/webpack.config.js && webpack --color --config web/webpack.config.js",
|
||||
"build": "npm run build:typings && node scripts/build-modules.js",
|
||||
"build:typings": "node scripts/build-typings.js",
|
||||
"watch": "cross-env TABBY_DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
|
||||
"start:prod": "electron app --debug",
|
||||
"prod": "cross-env TABBY_DEV=1 electron app",
|
||||
"docs": "typedoc --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
|
||||
"docs": "typedoc --emit --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --emit --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --emit --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --emit --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
|
||||
"lint": "eslint --ext ts */src */lib",
|
||||
"postinstall": "node ./scripts/install-deps.js",
|
||||
"patch": "patch-package; cd web; patch-package"
|
||||
|
@@ -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() {
|
||||
|
22
scripts/build-modules.js
Executable file
22
scripts/build-modules.js
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
const webpack = require('webpack')
|
||||
const { promisify } = require('util')
|
||||
|
||||
const configs = [
|
||||
'../app/webpack.main.config.js',
|
||||
'../app/webpack.config.js',
|
||||
...vars.allPackages.map(x => `../${x}/webpack.config.js`),
|
||||
]
|
||||
|
||||
;(async () => {
|
||||
for (const c of configs) {
|
||||
log.info('build', c)
|
||||
const stats = await promisify(webpack)(require(c))
|
||||
console.log(stats.toString({ colors: true }))
|
||||
if (stats.hasErrors()) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
})()
|
12
scripts/generate-icon-metadata.js
Executable file
12
scripts/generate-icon-metadata.js
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
const jsYaml = require('js-yaml')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const metadata = jsYaml.load(fs.readFileSync(path.resolve(__dirname, '../node_modules/@fortawesome/fontawesome-free/metadata/icons.yml')))
|
||||
|
||||
let result = {}
|
||||
for (let key in metadata) {
|
||||
result[key] = metadata[key].styles.map(x => x[0])
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, '../tabby-core/src/icons.json'), JSON.stringify(result))
|
@@ -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')
|
||||
|
||||
@@ -17,17 +20,17 @@ sh.cd('web')
|
||||
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('..')
|
||||
vars.allPackages.forEach(plugin => {
|
||||
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.forEach(plugin => {
|
||||
vars.allPackages.forEach(plugin => {
|
||||
log.info('bump', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec('npm --no-git-tag-version version ' + vars.version)
|
||||
|
@@ -9,7 +9,7 @@ sh.exec(`${sentryCli} releases new ${vars.version}`)
|
||||
if (process.platform === 'darwin') {
|
||||
for (const path of [
|
||||
'app/node_modules/@serialport/bindings/build/Release/bindings.node',
|
||||
'app/node_modules/node-pty/build/Release/pty.node',
|
||||
'app/node_modules/@tabby-gang/node-pty/build/Release/pty.node',
|
||||
'app/node_modules/fontmanager-redux/build/Release/fontmanager.node',
|
||||
'app/node_modules/macos-native-processlist/build/Release/native.node',
|
||||
]) {
|
||||
|
@@ -5,28 +5,36 @@ const childProcess = require('child_process')
|
||||
|
||||
const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
|
||||
|
||||
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
|
||||
exports.version = childProcess.execSync('git describe --tags', { encoding:'utf-8' })
|
||||
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 = [
|
||||
'tabby-core',
|
||||
'tabby-settings',
|
||||
'tabby-terminal',
|
||||
'tabby-electron',
|
||||
'tabby-local',
|
||||
'tabby-web',
|
||||
'tabby-community-color-schemes',
|
||||
'tabby-plugin-manager',
|
||||
'tabby-ssh',
|
||||
'tabby-serial',
|
||||
'tabby-core',
|
||||
'tabby-settings',
|
||||
'tabby-terminal',
|
||||
'tabby-web',
|
||||
'tabby-community-color-schemes',
|
||||
'tabby-ssh',
|
||||
'tabby-serial',
|
||||
'tabby-telnet',
|
||||
'tabby-electron',
|
||||
'tabby-local',
|
||||
'tabby-plugin-manager',
|
||||
]
|
||||
|
||||
exports.allPackages = [
|
||||
...exports.builtinPlugins,
|
||||
'web',
|
||||
'tabby-web-demo',
|
||||
]
|
||||
|
||||
exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.electronVersion = electronInfo.version
|
||||
|
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.144",
|
||||
"version": "1.0.156",
|
||||
"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.144",
|
||||
"version": "1.0.156",
|
||||
"description": "Tabby core",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
@@ -19,14 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"clone-deep": "^4.0.1",
|
||||
"core-js": "^3.1.2",
|
||||
"deep-equal": "^2.0.5",
|
||||
"deepmerge": "^4.1.1",
|
||||
"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",
|
||||
|
@@ -2,7 +2,7 @@ export { BaseComponent, SubscriptionContainer } from '../components/base.compone
|
||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||
export { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||
export { TabRecoveryProvider, RecoveredTab, RecoveryToken } from './tabRecovery'
|
||||
export { TabRecoveryProvider, RecoveryToken } from './tabRecovery'
|
||||
export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider'
|
||||
@@ -16,18 +16,22 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||
export { HostWindowService } from './hostWindow'
|
||||
export { HostAppService, Platform } from './hostApp'
|
||||
export { FileProvider } from './fileProvider'
|
||||
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'
|
||||
export { HotkeysService } from '../services/hotkeys.service'
|
||||
export { KeyEventData, KeyName, Keystroke } from '../services/hotkeys.util'
|
||||
export { NotificationsService } from '../services/notifications.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { ProfilesService } from '../services/profiles.service'
|
||||
export { SelectorService } from '../services/selector.service'
|
||||
export { TabsService } from '../services/tabs.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'
|
||||
|
@@ -13,6 +13,7 @@ export interface MessageBoxOptions {
|
||||
detail?: string
|
||||
buttons: string[]
|
||||
defaultId?: number
|
||||
cancelId?: number
|
||||
}
|
||||
|
||||
export interface MessageBoxResult {
|
||||
@@ -21,6 +22,7 @@ export interface MessageBoxResult {
|
||||
|
||||
export abstract class FileTransfer {
|
||||
abstract getName (): string
|
||||
abstract getMode (): number
|
||||
abstract getSize (): number
|
||||
abstract close (): void
|
||||
|
||||
@@ -95,7 +97,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 +190,10 @@ export class HTMLFileUpload extends FileUpload {
|
||||
return this.file.name
|
||||
}
|
||||
|
||||
getMode (): number {
|
||||
return 0o644
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.file.size
|
||||
}
|
||||
|
60
tabby-core/src/api/profileProvider.ts
Normal file
60
tabby-core/src/api/profileProvider.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/* 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
|
||||
type: string
|
||||
name: string
|
||||
group?: string
|
||||
options: any
|
||||
|
||||
icon?: string
|
||||
color?: string
|
||||
disableDynamicTitle: boolean
|
||||
|
||||
weight: number
|
||||
isBuiltin: boolean
|
||||
isTemplate: boolean
|
||||
}
|
||||
|
||||
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<P extends Profile> {
|
||||
id: string
|
||||
name: string
|
||||
supportsQuickConnect = false
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||
configDefaults = {}
|
||||
|
||||
abstract getBuiltinProfiles (): Promise<PartialProfile<P>[]>
|
||||
|
||||
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
|
||||
|
||||
getSuggestedName (profile: PartialProfile<P>): string|null {
|
||||
return null
|
||||
}
|
||||
|
||||
abstract getDescription (profile: PartialProfile<P>): string
|
||||
|
||||
quickConnect (query: string): PartialProfile<P>|null {
|
||||
return null
|
||||
}
|
||||
|
||||
deleteProfile (profile: P): void { }
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
export interface SelectorOption<T> {
|
||||
name: string
|
||||
description?: string
|
||||
group?: string
|
||||
result?: T
|
||||
icon?: string
|
||||
freeInputPattern?: string
|
||||
color?: string
|
||||
callback?: (string?) => void
|
||||
}
|
||||
|
@@ -1,17 +1,6 @@
|
||||
import deepClone from 'clone-deep'
|
||||
import { TabComponentType } from '../services/tabs.service'
|
||||
|
||||
export interface RecoveredTab {
|
||||
/**
|
||||
* Component type to be instantiated
|
||||
*/
|
||||
type: TabComponentType
|
||||
|
||||
/**
|
||||
* Component instance inputs
|
||||
*/
|
||||
options?: any
|
||||
}
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { NewTabParameters } from '../services/tabs.service'
|
||||
|
||||
export interface RecoveryToken {
|
||||
[_: string]: any
|
||||
@@ -35,19 +24,20 @@ export interface RecoveryToken {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class TabRecoveryProvider {
|
||||
export abstract class TabRecoveryProvider <T extends BaseTabComponent> {
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[boolean]] whether this [[TabRecoveryProvider]] can recover a tab from this token
|
||||
*/
|
||||
|
||||
abstract applicableTo (recoveryToken: RecoveryToken): Promise<boolean>
|
||||
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs
|
||||
* @returns [[NewTabParameters]] descriptor containing tab type and component inputs
|
||||
* or `null` if this token is from a different tab type or is not supported
|
||||
*/
|
||||
abstract recover (recoveryToken: RecoveryToken): Promise<RecoveredTab>
|
||||
abstract recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<T>>
|
||||
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
|
58
tabby-core/src/buttonProvider.ts
Normal file
58
tabby-core/src/buttonProvider.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
||||
import { HostAppService, Platform } from './api/hostApp'
|
||||
import { PartialProfile, Profile } from './api/profileProvider'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { HotkeysService } from './services/hotkeys.service'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
private profilesService: ProfilesService,
|
||||
private config: ConfigService,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
hotkeys.hotkey$.subscribe(hotkey => {
|
||||
if (hotkey === 'profile-selector') {
|
||||
this.activate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async activate () {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
if (profile) {
|
||||
this.launchProfile(profile)
|
||||
}
|
||||
}
|
||||
|
||||
async launchProfile (profile: PartialProfile<Profile>) {
|
||||
await this.profilesService.openNewTabForProfile(profile)
|
||||
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['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 = []
|
||||
}
|
||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||
}
|
||||
|
||||
provide (): ToolbarButton[] {
|
||||
return [{
|
||||
icon: this.hostApp.platform === Platform.Web
|
||||
? require('./icons/plus.svg')
|
||||
: require('./icons/profiles.svg'),
|
||||
title: 'New tab with profile',
|
||||
click: () => this.activate(),
|
||||
}]
|
||||
}
|
||||
}
|
@@ -1,6 +1,41 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from './api/hostApp'
|
||||
import { CLIHandler, CLIEvent } from './api/cli'
|
||||
import { HostWindowService } from './api/hostWindow'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
|
||||
@Injectable()
|
||||
export class ProfileCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = 0
|
||||
|
||||
constructor (
|
||||
private profiles: ProfilesService,
|
||||
private hostWindow: HostWindowService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
const op = event.argv._[0]
|
||||
|
||||
if (op === 'profile') {
|
||||
this.handleOpenProfile(event.argv.profileName)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async handleOpenProfile (profileName: string) {
|
||||
const profile = (await this.profiles.getProfiles()).find(x => x.name === profileName)
|
||||
if (!profile) {
|
||||
console.error('Requested profile', profileName, 'not found')
|
||||
return
|
||||
}
|
||||
this.profiles.openNewTabForProfile(profile)
|
||||
this.hostWindow.bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LastCLIHandler extends CLIHandler {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
title-bar(
|
||||
*ngIf='ready && !hostWindow.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullScreen'
|
||||
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullscreen'
|
||||
)
|
||||
|
||||
.content(
|
||||
@@ -10,19 +10,17 @@ title-bar(
|
||||
)
|
||||
.tab-bar
|
||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
|
||||
&& !hostWindow.isFullScreen \
|
||||
&& !hostWindow.isFullscreen \
|
||||
&& config.store.appearance.frame == "thin" \
|
||||
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||
.tabs(
|
||||
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()',
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
@@ -30,7 +28,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 +107,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,
|
||||
@@ -80,7 +82,7 @@ export class AppRootComponent {
|
||||
this.logger = log.create('main')
|
||||
this.logger.info('v', platform.getAppVersion())
|
||||
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey: string) => {
|
||||
this.hotkeys.hotkey$.subscribe((hotkey: string) => {
|
||||
if (hotkey.startsWith('tab-')) {
|
||||
const index = parseInt(hotkey.split('-')[1])
|
||||
if (index <= this.app.tabs.length) {
|
||||
@@ -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,17 +182,6 @@ export class AppRootComponent {
|
||||
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
|
||||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.tabsDragging = false
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
async generateButtonSubmenu (button: ToolbarButton) {
|
||||
if (button.submenu) {
|
||||
button.submenuItems = await button.submenu()
|
||||
@@ -194,6 +192,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 { Observable, Subject, distinctUntilChanged, debounceTime } from 'rxjs'
|
||||
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,11 +65,13 @@ 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 }
|
||||
get progress$ (): Observable<number|null> { return this.progress }
|
||||
get activity$ (): Observable<boolean> { return this.activity }
|
||||
get titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) }
|
||||
get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) }
|
||||
get activity$ (): Observable<boolean> { return this.activity.pipe(debounceTime(500)) }
|
||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||
|
||||
@@ -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
|
||||
|
@@ -1,5 +1,5 @@
|
||||
.modal-body
|
||||
input.form-control(
|
||||
input.form-control.form-control-lg(
|
||||
type='text',
|
||||
[(ngModel)]='filter',
|
||||
autofocus,
|
||||
@@ -7,20 +7,25 @@
|
||||
(ngModelChange)='onFilterChange()'
|
||||
)
|
||||
|
||||
.list-group(*ngIf='filteredOptions.length')
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
#item,
|
||||
(click)='selectOption(option)',
|
||||
[class.active]='selectedIndex == i',
|
||||
*ngFor='let option of filteredOptions; let i = index'
|
||||
)
|
||||
i.icon(
|
||||
class='fa-fw fas fa-{{option.icon}}',
|
||||
*ngIf='!iconIsSVG(option.icon)'
|
||||
.list-group.list-group-light(*ngIf='filteredOptions.length')
|
||||
ng-container(*ngFor='let option of filteredOptions; let i = index')
|
||||
label.group-header(
|
||||
*ngIf='hasGroups && option.group !== filteredOptions[i - 1]?.group'
|
||||
) {{option.group}}
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
#item,
|
||||
(click)='selectOption(option)',
|
||||
[class.active]='selectedIndex == i'
|
||||
)
|
||||
.icon(
|
||||
[fastHtmlBind]='option.icon',
|
||||
*ngIf='iconIsSVG(option.icon)'
|
||||
)
|
||||
.mr-2.title {{getOptionText(option)}}
|
||||
.text-muted {{option.description}}
|
||||
i.icon(
|
||||
class='fa-fw {{option.icon}}',
|
||||
style='color: {{option.color}}',
|
||||
*ngIf='!iconIsSVG(option.icon)'
|
||||
)
|
||||
.icon(
|
||||
[fastHtmlBind]='option.icon',
|
||||
style='color: {{option.color}}',
|
||||
*ngIf='iconIsSVG(option.icon)'
|
||||
)
|
||||
.title.mr-2 {{getOptionText(option)}}
|
||||
.description.no-wrap.text-muted {{option.description}}
|
||||
|
@@ -9,6 +9,11 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
padding: 0 1rem;
|
||||
margin: 20px 0 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
margin-right: 0.25rem;
|
||||
@@ -16,9 +21,13 @@
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
input {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ export class SelectorModalComponent<T> {
|
||||
@Input() filter = ''
|
||||
@Input() name: string
|
||||
@Input() selectedIndex = 0
|
||||
hasGroups = false
|
||||
@ViewChildren('item') itemChildren: QueryList<ElementRef>
|
||||
|
||||
constructor (
|
||||
@@ -22,6 +23,7 @@ export class SelectorModalComponent<T> {
|
||||
|
||||
ngOnInit (): void {
|
||||
this.onFilterChange()
|
||||
this.hasGroups = this.options.some(x => x.group)
|
||||
}
|
||||
|
||||
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void {
|
||||
@@ -48,15 +50,23 @@ export class SelectorModalComponent<T> {
|
||||
onFilterChange (): void {
|
||||
const f = this.filter.trim().toLowerCase()
|
||||
if (!f) {
|
||||
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
|
||||
this.filteredOptions = this.options.slice()
|
||||
.sort((a, b) => a.group?.localeCompare(b.group ?? '') ?? 0)
|
||||
.filter(x => !x.freeInputPattern)
|
||||
} else {
|
||||
const terms = f.split(' ')
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
this.filteredOptions = this.options.filter(x => x.freeInputPattern ?? (x.name + (x.description ?? '')).toLowerCase().includes(f))
|
||||
this.filteredOptions = this.options.filter(x => x.freeInputPattern ?? this.filterMatches(x, terms))
|
||||
}
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex)
|
||||
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)
|
||||
}
|
||||
|
||||
filterMatches (option: SelectorOption<T>, terms: string[]): boolean {
|
||||
const content = (option.group ?? '') + option.name + (option.description ?? '')
|
||||
return terms.every(term => content.toLowerCase().includes(term))
|
||||
}
|
||||
|
||||
getOptionText (option: SelectorOption<T>): string {
|
||||
if (option.freeInputPattern) {
|
||||
return option.freeInputPattern.replace('%s', this.filter)
|
||||
|
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
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core'
|
||||
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
|
||||
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
|
||||
import { TabsService } from '../services/tabs.service'
|
||||
import { TabRecoveryProvider, RecoveryToken } from '../api/tabRecovery'
|
||||
import { TabsService, NewTabParameters } from '../services/tabs.service'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
|
||||
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type SplitOrientation = 'v' | 'h'
|
||||
export type SplitDirection = 'r' | 't' | 'b' | 'l'
|
||||
|
||||
/**
|
||||
* Describes a horizontal or vertical split row or column
|
||||
@@ -93,13 +93,13 @@ export class SplitContainer {
|
||||
return s
|
||||
}
|
||||
|
||||
async serialize (): Promise<RecoveryToken> {
|
||||
async serialize (tabsRecovery: TabRecoveryService): Promise<RecoveryToken> {
|
||||
const children: any[] = []
|
||||
for (const child of this.children) {
|
||||
if (child instanceof SplitContainer) {
|
||||
children.push(await child.serialize())
|
||||
children.push(await child.serialize(tabsRecovery))
|
||||
} else {
|
||||
children.push(await child.getRecoveryToken())
|
||||
children.push(await tabsRecovery.getFullRecoveryToken(child))
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -123,6 +123,25 @@ export interface SplitSpannerInfo {
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tab drop zone
|
||||
*/
|
||||
export type SplitDropZoneInfo = {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
} & ({
|
||||
type: 'absolute'
|
||||
container: SplitContainer
|
||||
position: number
|
||||
} | {
|
||||
type: 'relative'
|
||||
relativeTo?: BaseTabComponent|SplitContainer
|
||||
side: SplitDirection
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||
* You'll mainly encounter it inside [[AppService]].tabs
|
||||
@@ -137,6 +156,21 @@ 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>
|
||||
<split-tab-pane-label
|
||||
*ngFor='let tab of getAllTabs()'
|
||||
cdkDropList
|
||||
cdkAutoDropGroup='app-tabs'
|
||||
[tab]='tab'
|
||||
[parent]='this'
|
||||
>
|
||||
</split-tab-pane-label>
|
||||
`,
|
||||
styles: [require('./splitTab.component.scss')],
|
||||
})
|
||||
@@ -157,6 +191,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
/** @hidden */
|
||||
_spanners: SplitSpannerInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_dropZones: SplitDropZoneInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_allFocusMode = false
|
||||
|
||||
@@ -166,12 +203,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 }
|
||||
|
||||
/**
|
||||
@@ -209,7 +253,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
})
|
||||
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
|
||||
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => {
|
||||
if (!this.hasFocus || !this.focusedTab) {
|
||||
return
|
||||
}
|
||||
@@ -330,51 +374,77 @@ 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|SplitContainer|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()
|
||||
}
|
||||
|
||||
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
|
||||
let insertIndex = relative ? target.children.indexOf(relative) : -1
|
||||
if (thing instanceof BaseTabComponent) {
|
||||
if (thing.parent instanceof SplitTabComponent) {
|
||||
thing.parent.removeTab(thing)
|
||||
}
|
||||
thing.removeFromContainer()
|
||||
thing.parent = this
|
||||
}
|
||||
|
||||
let target = relative ? this.getParentOf(relative) : null
|
||||
if (!target) {
|
||||
// Rewrap the root container just in case the orientation isn't compatibile
|
||||
target = new SplitContainer()
|
||||
target.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
|
||||
target.children = [this.root]
|
||||
target.ratios = [1]
|
||||
this.root = target
|
||||
}
|
||||
|
||||
let insertIndex = relative
|
||||
? target.children.indexOf(relative) + ('tl'.includes(side) ? 0 : 1)
|
||||
: 'tl'.includes(side) ? 0 : -1
|
||||
|
||||
if (
|
||||
target.orientation === 'v' && ['l', 'r'].includes(side) ||
|
||||
target.orientation === 'h' && ['t', 'b'].includes(side)
|
||||
) {
|
||||
// Inserting into a container but the orientation isn't compatible
|
||||
const newContainer = new SplitContainer()
|
||||
newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
|
||||
newContainer.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
|
||||
newContainer.children = relative ? [relative] : []
|
||||
newContainer.ratios = [1]
|
||||
target.children[insertIndex] = newContainer
|
||||
target.children.splice(relative ? target.children.indexOf(relative) : -1, 1, newContainer)
|
||||
target = newContainer
|
||||
insertIndex = 0
|
||||
}
|
||||
|
||||
if (insertIndex === -1) {
|
||||
insertIndex = 0
|
||||
} else {
|
||||
insertIndex += side === 'l' || side === 't' ? 0 : 1
|
||||
insertIndex = 'tl'.includes(side) ? 0 : 1
|
||||
}
|
||||
|
||||
for (let i = 0; i < target.children.length; i++) {
|
||||
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||
}
|
||||
if (insertIndex === -1) {
|
||||
insertIndex = target.ratios.length
|
||||
}
|
||||
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
||||
target.children.splice(insertIndex, 0, 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 +456,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 +470,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
|
||||
*/
|
||||
@@ -453,7 +539,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
async splitTab (tab: BaseTabComponent, dir: SplitDirection): Promise<BaseTabComponent|null> {
|
||||
const newTab = await this.tabsService.duplicate(tab)
|
||||
if (newTab) {
|
||||
this.addTab(newTab, tab, dir)
|
||||
await this.addTab(newTab, tab, dir)
|
||||
}
|
||||
return newTab
|
||||
}
|
||||
@@ -484,7 +570,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
|
||||
/** @hidden */
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return this.root.serialize()
|
||||
return this.root.serialize(this.tabRecovery)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@@ -498,6 +584,20 @@ 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
|
||||
}
|
||||
|
||||
if (zone.type === 'relative') {
|
||||
this.add(tab, zone.relativeTo ?? null, zone.side)
|
||||
} else {
|
||||
this.add(tab, zone.container.children[zone.position], zone.container.orientation === 'h' ? 'r' : 'b')
|
||||
}
|
||||
this.tabAdopted.next(tab)
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
super.destroy()
|
||||
for (const x of this.getAllTabs()) {
|
||||
@@ -508,20 +608,24 @@ 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
|
||||
this.viewRefs.set(tab, ref)
|
||||
private updateTitle (): void {
|
||||
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
|
||||
}
|
||||
|
||||
private attachTabView (tab: BaseTabComponent) {
|
||||
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))
|
||||
tab.subscribeUntilDestroyed(tab.titleChange$, () => this.updateTitle())
|
||||
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
|
||||
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
|
||||
if (tab.title) {
|
||||
this.setTitle(tab.title)
|
||||
this.updateTitle()
|
||||
}
|
||||
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
|
||||
this.recoveryStateChangedHint.next()
|
||||
@@ -531,17 +635,53 @@ 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) {
|
||||
const size = root.orientation === 'v' ? h : w
|
||||
const sizes = root.ratios.map(ratio => ratio * size)
|
||||
const thickness = 10
|
||||
|
||||
if (root === this.root) {
|
||||
this._dropZones.push({
|
||||
x: x - thickness / 2,
|
||||
y: y + thickness,
|
||||
w: thickness,
|
||||
h: h - thickness * 2,
|
||||
type: 'relative',
|
||||
side: 'l',
|
||||
})
|
||||
this._dropZones.push({
|
||||
x,
|
||||
y: y - thickness / 2,
|
||||
w,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
side: 't',
|
||||
})
|
||||
this._dropZones.push({
|
||||
x: x + w - thickness / 2,
|
||||
y: y + thickness,
|
||||
w: thickness,
|
||||
h: h - thickness * 2,
|
||||
type: 'relative',
|
||||
side: 'r',
|
||||
})
|
||||
this._dropZones.push({
|
||||
x,
|
||||
y: y + h - thickness / 2,
|
||||
w,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
side: 'b',
|
||||
})
|
||||
}
|
||||
|
||||
root.x = x
|
||||
root.y = y
|
||||
@@ -577,8 +717,63 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += sizes[i]
|
||||
|
||||
if (i !== root.ratios.length - 1) {
|
||||
// Spanner area
|
||||
this._dropZones.push({
|
||||
type: 'relative',
|
||||
relativeTo: root.children[i],
|
||||
side: root.orientation === 'v' ? 'b': 'r',
|
||||
x: root.orientation === 'v' ? childX + thickness : childX + offset - thickness / 2,
|
||||
y: root.orientation === 'v' ? childY + offset - thickness / 2 : childY + thickness,
|
||||
w: root.orientation === 'v' ? childW - thickness * 2 : thickness,
|
||||
h: root.orientation === 'v' ? thickness : childH - thickness * 2,
|
||||
})
|
||||
}
|
||||
|
||||
// Sides
|
||||
if (root.orientation === 'v') {
|
||||
this._dropZones.push({
|
||||
x: childX,
|
||||
y: childY + thickness,
|
||||
w: thickness,
|
||||
h: childH - thickness * 2,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'l',
|
||||
})
|
||||
this._dropZones.push({
|
||||
x: childX + w - thickness,
|
||||
y: childY + thickness,
|
||||
w: thickness,
|
||||
h: childH - thickness * 2,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'r',
|
||||
})
|
||||
} else {
|
||||
this._dropZones.push({
|
||||
x: childX + thickness,
|
||||
y: childY,
|
||||
w: childW - thickness * 2,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 't',
|
||||
})
|
||||
this._dropZones.push({
|
||||
x: childX + thickness,
|
||||
y: childY + childH - thickness,
|
||||
w: childW - thickness * 2,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'b',
|
||||
})
|
||||
}
|
||||
|
||||
if (i !== 0) {
|
||||
this._spanners.push({
|
||||
container: root,
|
||||
@@ -594,6 +789,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)
|
||||
@@ -601,7 +799,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
} else {
|
||||
const recovered = await this.tabRecovery.recoverTab(childState, duplicate)
|
||||
if (recovered) {
|
||||
const tab = this.tabsService.create(recovered.type, recovered.options)
|
||||
const tab = this.tabsService.create(recovered)
|
||||
children.push(tab)
|
||||
tab.parent = this
|
||||
this.attachTabView(tab)
|
||||
@@ -618,16 +816,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SplitTabRecoveryProvider extends TabRecoveryProvider<SplitTabComponent> {
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:split-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
|
||||
async recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<SplitTabComponent>> {
|
||||
return {
|
||||
type: SplitTabComponent,
|
||||
options: { _recoveredState: recoveryToken },
|
||||
inputs: { _recoveredState: recoveryToken },
|
||||
}
|
||||
}
|
||||
|
||||
|
39
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
39
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
: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%;
|
||||
opacity: 0;
|
||||
|
||||
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 {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, .5);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::ng-deep tab-header {
|
||||
// placeholders
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
54
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
54
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/* 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.dropZone.type === 'relative' || tab !== this.dropZone.container.children[this.dropZone.position])
|
||||
this.layout()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.layout()
|
||||
}
|
||||
|
||||
layout () {
|
||||
this.setDimensions(
|
||||
this.dropZone.x,
|
||||
this.dropZone.y,
|
||||
this.dropZone.w,
|
||||
this.dropZone.h,
|
||||
)
|
||||
}
|
||||
}
|
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
:host {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, .25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: .125s opacity cubic-bezier(0.86, 0, 0.07, 1);
|
||||
}
|
||||
|
||||
div {
|
||||
background: rgba(0, 0, 0, .7);
|
||||
padding: 20px 30px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
:host.active {
|
||||
opacity: 1;
|
||||
|
||||
> div {
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
cursor: move;
|
||||
}
|
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, HostBinding, ElementRef } from '@angular/core'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'split-tab-pane-label',
|
||||
template: `
|
||||
<div
|
||||
cdkDrag
|
||||
[cdkDragData]='tab'
|
||||
(cdkDragStarted)='onTabDragStart(tab)'
|
||||
(cdkDragEnded)='onTabDragEnd()'
|
||||
>
|
||||
<i class="fa fa-window-maximize mr-3"></i>
|
||||
<label>{{tab.title}}</label>
|
||||
</div>
|
||||
`,
|
||||
styles: [require('./splitTabPaneLabel.component.scss')],
|
||||
})
|
||||
export class SplitTabPaneLabelComponent extends SelfPositioningComponent {
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() parent: BaseTabComponent
|
||||
@HostBinding('class.active') isActive = false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
element: ElementRef,
|
||||
hotkeys: HotkeysService,
|
||||
private app: AppService,
|
||||
) {
|
||||
super(element)
|
||||
this.subscribeUntilDestroyed(hotkeys.hotkey$, hk => {
|
||||
if (hk === 'rearrange-panes' && this.parent.hasFocus) {
|
||||
this.isActive = true
|
||||
this.layout()
|
||||
}
|
||||
})
|
||||
this.subscribeUntilDestroyed(hotkeys.hotkeyOff$, hk => {
|
||||
if (hk === 'rearrange-panes') {
|
||||
this.isActive = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.layout()
|
||||
}
|
||||
|
||||
onTabDragStart (tab: BaseTabComponent): void {
|
||||
this.app.emitTabDragStarted(tab)
|
||||
}
|
||||
|
||||
onTabDragEnd (): void {
|
||||
setTimeout(() => {
|
||||
this.app.emitTabDragEnded()
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
layout () {
|
||||
const tabElement: HTMLElement|undefined = this.tab.viewContainerEmbeddedRef?.rootNodes[0]
|
||||
|
||||
if (!tabElement) {
|
||||
// being destroyed
|
||||
return
|
||||
}
|
||||
|
||||
this.setDimensions(
|
||||
tabElement.offsetLeft,
|
||||
tabElement.offsetTop,
|
||||
tabElement.clientWidth,
|
||||
tabElement.clientHeight,
|
||||
'px'
|
||||
)
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
transition: 0.125s background;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
|
||||
&.v {
|
||||
cursor: ns-resize;
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -3,9 +3,9 @@ div
|
||||
h1.tabby-title Tabby
|
||||
sup α
|
||||
|
||||
.list-group
|
||||
.list-group.list-group-light
|
||||
a.list-group-item.list-group-item-action.d-flex(
|
||||
*ngFor='let button of getButtons()',
|
||||
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
|
||||
(click)='button.click()',
|
||||
)
|
||||
.d-flex.align-self-center([innerHTML]='sanitizeIcon(button.icon)')
|
||||
@@ -13,10 +13,10 @@ div
|
||||
|
||||
footer.d-flex.align-items-center
|
||||
.btn-group.mr-auto
|
||||
button.btn.btn-secondary((click)='homeBase.openGitHub()')
|
||||
button.btn.btn-dark((click)='homeBase.openGitHub()')
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
||||
button.btn.btn-dark((click)='homeBase.reportBug()')
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
|
||||
|
@@ -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,
|
||||
@@ -32,4 +32,8 @@ export class StartPageComponent {
|
||||
sanitizeIcon (icon?: string): any {
|
||||
return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
|
||||
}
|
||||
|
||||
buttonsTrackBy (btn: ToolbarButton): any {
|
||||
return btn.title + btn.icon
|
||||
}
|
||||
}
|
||||
|
@@ -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: [
|
||||
@@ -19,20 +16,22 @@ import { BaseTabComponent } from '../components/baseTab.component'
|
||||
export class TabBodyComponent implements OnChanges {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder?: ViewContainerRef
|
||||
|
||||
ngOnChanges (changes) {
|
||||
if (changes.tab) {
|
||||
if (this.placeholder) {
|
||||
this.placeholder.detach()
|
||||
}
|
||||
this.placeholder?.detach()
|
||||
setImmediate(() => {
|
||||
this.placeholder.insert(this.tab.hostView)
|
||||
this.placeholder?.insert(this.tab.hostView)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
detach () {
|
||||
this.placeholder?.detach()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.placeholder.detach()
|
||||
this.placeholder?.detach()
|
||||
}
|
||||
}
|
||||
|
@@ -2,9 +2,18 @@
|
||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||
.activity-indicator(*ngIf='tab.activity$|async')
|
||||
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex', #handle) {{index + 1}}
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
|
||||
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform !== Platform.macOS') {{index + 1}}
|
||||
|
||||
.name(
|
||||
[title]='tab.customTitle || tab.title',
|
||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||
cdkDrag,
|
||||
cdkDragRootElement='tab-header',
|
||||
[cdkDragData]='tab',
|
||||
(cdkDragStarted)='onTabDragStart(tab)',
|
||||
(cdkDragEnded)='onTabDragEnd()',
|
||||
) {{tab.customTitle || tab.title}}
|
||||
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
||||
|
||||
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,21 +23,20 @@ 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()
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, (hotkey) => {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.hotkey$, (hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
if (hotkey === 'rename-tab') {
|
||||
this.showRenameTabModal()
|
||||
@@ -61,18 +54,13 @@ 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
|
||||
modal.result.then(result => {
|
||||
this.tab.setTitle(result)
|
||||
this.tab.customTitle = result
|
||||
this.app.emitTabsChanged()
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
@@ -85,6 +73,17 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
return items.slice(1)
|
||||
}
|
||||
|
||||
onTabDragStart (tab: BaseTabComponent) {
|
||||
this.app.emitTabDragStarted(tab)
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.app.emitTabDragEnded()
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
@HostBinding('class.flex-width') get isFlexWidthEnabled (): boolean {
|
||||
return this.config.store.appearance.flexTabs
|
||||
}
|
||||
|
@@ -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()')
|
||||
|
@@ -1,11 +1,14 @@
|
||||
:host {
|
||||
min-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.transfer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0 5px 25px;
|
||||
width: 100%;
|
||||
|
||||
.icon {
|
||||
padding: 4px 7px;
|
||||
@@ -16,12 +19,16 @@
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
margin-right: auto;
|
||||
margin-bottom: 3px;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -43,6 +43,7 @@ export class TransfersMenuComponent {
|
||||
message: 'There are active file transfers',
|
||||
buttons: ['Abort all', 'Do not abort'],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
})).response === 1) {
|
||||
return
|
||||
}
|
||||
|
@@ -19,18 +19,6 @@
|
||||
.description Toggles the Tabby window visibility
|
||||
toggle([(ngModel)]='enableGlobalHotkey')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable #[strong SSH] plugin
|
||||
.description Adds an SSH connection manager UI to Tabby
|
||||
toggle([(ngModel)]='enableSSH')
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable #[strong Serial] plugin
|
||||
.description Allows attaching Tabby to serial ports
|
||||
toggle([(ngModel)]='enableSerial')
|
||||
|
||||
|
||||
.text-center.mt-5
|
||||
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again
|
||||
|
@@ -11,8 +11,6 @@ import { HostWindowService } from '../api/hostWindow'
|
||||
styles: [require('./welcomeTab.component.scss')],
|
||||
})
|
||||
export class WelcomeTabComponent extends BaseTabComponent {
|
||||
enableSSH = false
|
||||
enableSerial = false
|
||||
enableGlobalHotkey = true
|
||||
|
||||
constructor (
|
||||
@@ -21,23 +19,15 @@ export class WelcomeTabComponent extends BaseTabComponent {
|
||||
) {
|
||||
super()
|
||||
this.setTitle('Welcome')
|
||||
this.enableSSH = !config.store.pluginBlacklist.includes('ssh')
|
||||
this.enableSerial = !config.store.pluginBlacklist.includes('serial')
|
||||
}
|
||||
|
||||
closeAndDisable () {
|
||||
async closeAndDisable () {
|
||||
this.config.store.enableWelcomeTab = false
|
||||
this.config.store.pluginBlacklist = []
|
||||
if (!this.enableSSH) {
|
||||
this.config.store.pluginBlacklist.push('ssh')
|
||||
}
|
||||
if (!this.enableSerial) {
|
||||
this.config.store.pluginBlacklist.push('serial')
|
||||
}
|
||||
if (!this.enableGlobalHotkey) {
|
||||
this.config.store.hotkeys['toggle-window'] = []
|
||||
}
|
||||
this.config.save()
|
||||
await this.config.save()
|
||||
this.hostWindow.reload()
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -18,6 +18,8 @@ hotkeys:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
rearrange-panes:
|
||||
- 'Ctrl-Shift'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
@@ -69,4 +71,8 @@ hotkeys:
|
||||
pane-maximize:
|
||||
- 'Ctrl-Alt-Enter'
|
||||
close-pane: []
|
||||
switch-profile:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-T'
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -16,6 +16,8 @@ hotkeys:
|
||||
- '⌘-Shift-Left'
|
||||
move-tab-right:
|
||||
- '⌘-Shift-Right'
|
||||
rearrange-panes:
|
||||
- '⌘-Shift'
|
||||
tab-1:
|
||||
- '⌘-1'
|
||||
tab-2:
|
||||
@@ -68,4 +70,8 @@ hotkeys:
|
||||
- '⌘-⌥-Enter'
|
||||
close-pane:
|
||||
- '⌘-Shift-W'
|
||||
profile-selector:
|
||||
- '⌘-E'
|
||||
switch-profile:
|
||||
- '⌘-Shift-E'
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -19,6 +19,8 @@ hotkeys:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
rearrange-panes:
|
||||
- 'Ctrl-Shift'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
@@ -70,4 +72,8 @@ hotkeys:
|
||||
pane-maximize:
|
||||
- 'Ctrl-Alt-Enter'
|
||||
close-pane: []
|
||||
switch-profile:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-T'
|
||||
pluginBlacklist: []
|
||||
|
@@ -15,7 +15,17 @@ appearance:
|
||||
vibrancy: true
|
||||
vibrancyType: 'blur'
|
||||
terminal:
|
||||
recoverTabs: true
|
||||
showBuiltinProfiles: true
|
||||
showRecentProfiles: 3
|
||||
hotkeys:
|
||||
profile:
|
||||
__nonStructural: true
|
||||
profile-selectors:
|
||||
__nonStructural: true
|
||||
profiles: []
|
||||
profileDefaults:
|
||||
__nonStructural: true
|
||||
recoverTabs: true
|
||||
enableAnalytics: true
|
||||
enableWelcomeTab: true
|
||||
electronFlags:
|
||||
@@ -24,3 +34,4 @@ enableAutomaticUpdates: true
|
||||
version: 1
|
||||
vault: null
|
||||
encrypted: false
|
||||
enableExperimentalFeatures: false
|
||||
|
@@ -0,0 +1,19 @@
|
||||
import { Directive, ElementRef, AfterViewInit } from '@angular/core'
|
||||
|
||||
/** @hidden */
|
||||
@Directive({
|
||||
selector: '[alwaysVisibleTypeahead]',
|
||||
})
|
||||
export class AlwaysVisibleTypeaheadDirective implements AfterViewInit {
|
||||
constructor (private el: ElementRef) { }
|
||||
|
||||
ngAfterViewInit (): void {
|
||||
this.el.nativeElement.addEventListener('focus', e => {
|
||||
e.stopPropagation()
|
||||
setTimeout(() => {
|
||||
const inputEvent: Event = new Event('input')
|
||||
e.target.dispatchEvent(inputEvent)
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
}
|
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]
|
||||
}
|
||||
}
|
@@ -1,10 +1,16 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { PartialProfile, Profile } from './api'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class AppHotkeyProvider extends HotkeyProvider {
|
||||
hotkeys: HotkeyDescription[] = [
|
||||
{
|
||||
id: 'profile-selector',
|
||||
name: 'Show profile selector',
|
||||
},
|
||||
{
|
||||
id: 'toggle-fullscreen',
|
||||
name: 'Toggle fullscreen mode',
|
||||
@@ -41,6 +47,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
id: 'move-tab-right',
|
||||
name: 'Move tab to the right',
|
||||
},
|
||||
{
|
||||
id: 'rearrange-panes',
|
||||
name: 'Show pane labels (for rearranging)',
|
||||
},
|
||||
{
|
||||
id: 'tab-1',
|
||||
name: 'Tab 1',
|
||||
@@ -165,13 +175,36 @@ 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',
|
||||
},
|
||||
]
|
||||
|
||||
constructor (
|
||||
private profilesService: ProfilesService,
|
||||
) { super() }
|
||||
|
||||
async provide (): Promise<HotkeyDescription[]> {
|
||||
return this.hotkeys
|
||||
const profiles = await this.profilesService.getProfiles()
|
||||
return [
|
||||
...this.hotkeys,
|
||||
...profiles.map(profile => ({
|
||||
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
||||
name: `New tab: ${profile.name}`,
|
||||
})),
|
||||
...this.profilesService.getProviders().map(provider => ({
|
||||
id: `profile-selectors.${provider.id}`,
|
||||
name: `Show ${provider.name} profile selector`,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return profile.id!.replace(/\./g, '-')
|
||||
}
|
||||
}
|
||||
|
1
tabby-core/src/icons.json
Normal file
1
tabby-core/src/icons.json
Normal file
File diff suppressed because one or more lines are too long
1
tabby-core/src/icons/plus.svg
Normal file
1
tabby-core/src/icons/plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-plus fa-w-12 fa-3x" data-icon="plus" data-prefix="fal" focusable="false" role="img" viewBox="0 0 384 512"><path fill="#fff" stroke="none" stroke-width="1" d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z"/></svg>
|
After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 665 B After Width: | Height: | Size: 665 B |
@@ -5,11 +5,13 @@ 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'
|
||||
import { TabBodyComponent } from './components/tabBody.component'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SafeModeModalComponent } from './components/safeModeModal.component'
|
||||
import { StartPageComponent } from './components/startPage.component'
|
||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||
@@ -20,28 +22,34 @@ 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 { SplitTabPaneLabelComponent } from './components/splitTabPaneLabel.component'
|
||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||
|
||||
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 } from './api'
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService } from './api'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { VaultFileProvider } from './services/vault.service'
|
||||
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 { LastCLIHandler } from './cli'
|
||||
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 },
|
||||
@@ -52,10 +60,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 */
|
||||
@@ -67,11 +79,13 @@ const PROVIDERS = [
|
||||
NgbModule,
|
||||
NgxFilesizeModule,
|
||||
PerfectScrollbarModule,
|
||||
DndModule.forRoot(),
|
||||
DragDropModule,
|
||||
SortablejsModule.forRoot({ animation: 150 }),
|
||||
],
|
||||
declarations: [
|
||||
AppRootComponent as any,
|
||||
AppRootComponent,
|
||||
CheckboxComponent,
|
||||
PromptModalComponent,
|
||||
StartPageComponent,
|
||||
TabBodyComponent,
|
||||
TabHeaderComponent,
|
||||
@@ -82,15 +96,20 @@ const PROVIDERS = [
|
||||
SafeModeModalComponent,
|
||||
AutofocusDirective,
|
||||
FastHtmlBindDirective,
|
||||
AlwaysVisibleTypeaheadDirective,
|
||||
SelectorModalComponent,
|
||||
SplitTabComponent,
|
||||
SplitTabSpannerComponent,
|
||||
SplitTabDropZoneComponent,
|
||||
SplitTabPaneLabelComponent,
|
||||
UnlockVaultModalComponent,
|
||||
WelcomeTabComponent,
|
||||
TransfersMenuComponent,
|
||||
DropZoneDirective,
|
||||
CdkAutoDropGroup,
|
||||
],
|
||||
entryComponents: [
|
||||
PromptModalComponent,
|
||||
RenameTabModalComponent,
|
||||
SafeModeModalComponent,
|
||||
SelectorModalComponent,
|
||||
@@ -101,21 +120,81 @@ const PROVIDERS = [
|
||||
exports: [
|
||||
CheckboxComponent,
|
||||
ToggleComponent,
|
||||
PromptModalComponent,
|
||||
AutofocusDirective,
|
||||
DropZoneDirective,
|
||||
FastHtmlBindDirective,
|
||||
AlwaysVisibleTypeaheadDirective,
|
||||
SortablejsModule,
|
||||
DragDropModule,
|
||||
],
|
||||
})
|
||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
constructor (app: AppService, config: ConfigService, platform: PlatformService) {
|
||||
constructor (
|
||||
app: AppService,
|
||||
config: ConfigService,
|
||||
platform: PlatformService,
|
||||
hotkeys: HotkeysService,
|
||||
private profilesService: ProfilesService,
|
||||
private selector: SelectorService,
|
||||
) {
|
||||
app.ready$.subscribe(() => {
|
||||
if (config.store.enableWelcomeTab) {
|
||||
app.openNewTabRaw(WelcomeTabComponent)
|
||||
}
|
||||
config.ready$.toPromise().then(() => {
|
||||
if (config.store.enableWelcomeTab) {
|
||||
app.openNewTabRaw({ type: WelcomeTabComponent })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
platform.setErrorHandler(err => {
|
||||
console.error('Unhandled exception:', err)
|
||||
})
|
||||
|
||||
hotkeys.hotkey$.subscribe(async (hotkey) => {
|
||||
if (hotkey.startsWith('profile.')) {
|
||||
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||
const profiles = await profilesService.getProfiles()
|
||||
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
|
||||
if (profile) {
|
||||
profilesService.openNewTabForProfile(profile)
|
||||
}
|
||||
}
|
||||
if (hotkey.startsWith('profile-selectors.')) {
|
||||
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||
const provider = profilesService.getProviders().find(x => x.id === id)
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
this.showSelector(provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async showSelector (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
let profiles = await this.profilesService.getProfiles()
|
||||
|
||||
profiles = profiles.filter(x => !x.isTemplate && x.type === provider.id)
|
||||
|
||||
const options: SelectorOption<void>[] = profiles.map(p => ({
|
||||
...this.profilesService.selectorOptionForProfile(p),
|
||||
callback: () => this.profilesService.openNewTabForProfile(p),
|
||||
}))
|
||||
|
||||
if (provider.supportsQuickConnect) {
|
||||
options.push({
|
||||
name: 'Quick connect',
|
||||
freeInputPattern: 'Connect to "%s"...',
|
||||
icon: 'fas fa-arrow-right',
|
||||
callback: query => {
|
||||
const p = provider.quickConnect(query)
|
||||
if (p) {
|
||||
this.profilesService.openNewTabForProfile(p)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await this.selector.show('Select profile', options)
|
||||
}
|
||||
|
||||
static forRoot (): ModuleWithProviders<AppModule> {
|
||||
|
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()
|
||||
}
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
|
||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { Observable, Subject, AsyncSubject, takeUntil } from 'rxjs'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
@@ -13,7 +11,7 @@ import { HostAppService } from '../api/hostApp'
|
||||
|
||||
import { ConfigService } from './config.service'
|
||||
import { TabRecoveryService } from './tabRecovery.service'
|
||||
import { TabsService, TabComponentType } from './tabs.service'
|
||||
import { TabsService, NewTabParameters } from './tabs.service'
|
||||
import { SelectorService } from './selector.service'
|
||||
|
||||
class CompletionObserver {
|
||||
@@ -56,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>()
|
||||
@@ -64,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 }
|
||||
@@ -88,10 +90,10 @@ export class AppService {
|
||||
|
||||
config.ready$.toPromise().then(async () => {
|
||||
if (this.bootstrapData.isFirstWindow) {
|
||||
if (config.store.terminal.recoverTabs) {
|
||||
if (config.store.recoverTabs) {
|
||||
const tabs = await this.tabRecovery.recoverTabs()
|
||||
for (const tab of tabs) {
|
||||
this.openNewTabRaw(tab.type, tab.options)
|
||||
this.openNewTabRaw(tab)
|
||||
}
|
||||
}
|
||||
/** Continue to store the tabs even if the setting is currently off */
|
||||
@@ -133,27 +135,36 @@ 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
|
||||
*/
|
||||
openNewTabRaw (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent {
|
||||
const tab = this.tabsService.create(type, inputs)
|
||||
openNewTabRaw <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
|
||||
const tab = this.tabsService.create(params)
|
||||
this.addTabRaw(tab)
|
||||
return tab
|
||||
}
|
||||
@@ -162,9 +173,12 @@ export class AppService {
|
||||
* Adds a new tab while wrapping it in a SplitTabComponent
|
||||
* @param inputs Properties to be assigned on the new tab component instance
|
||||
*/
|
||||
openNewTab (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent {
|
||||
const splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent
|
||||
const tab = this.tabsService.create(type, inputs)
|
||||
openNewTab <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
|
||||
if (params.type as any === SplitTabComponent) {
|
||||
return this.openNewTabRaw(params)
|
||||
}
|
||||
const splitTab = this.tabsService.create({ type: SplitTabComponent })
|
||||
const tab = this.tabsService.create(params)
|
||||
splitTab.addTab(tab, null, 'r')
|
||||
this.addTabRaw(splitTab)
|
||||
return tab
|
||||
@@ -175,7 +189,7 @@ export class AppService {
|
||||
if (token) {
|
||||
const recoveredTab = await this.tabRecovery.recoverTab(token)
|
||||
if (recoveredTab) {
|
||||
const tab = this.tabsService.create(recoveredTab.type, recoveredTab.options)
|
||||
const tab = this.tabsService.create(recoveredTab)
|
||||
if (this.activeTab) {
|
||||
this.addTabRaw(tab, this.tabs.indexOf(this.activeTab) + 1)
|
||||
} else {
|
||||
@@ -346,6 +360,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,5 +1,8 @@
|
||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||
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'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { ConfigProvider } from '../api/configProvider'
|
||||
import { PlatformService } from '../api/platform'
|
||||
@@ -7,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
|
||||
|
||||
@@ -17,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 */
|
||||
@@ -45,38 +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 {
|
||||
if (isNonStructuralObjectMember(defaults[key])) {
|
||||
real[key] = { ...defaults[key] }
|
||||
// The object might be modified outside
|
||||
real[key] = this.__getDefault(key)
|
||||
delete real[key].__nonStructural
|
||||
return real[key]
|
||||
} else {
|
||||
return defaults[key]
|
||||
}
|
||||
return this.__getDefault(key)
|
||||
}
|
||||
}
|
||||
|
||||
this.setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
real[key] = value
|
||||
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 (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 { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__cleanup () { }
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -165,6 +194,10 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
async save (): Promise<void> {
|
||||
await this.ready$
|
||||
if (!this._store) {
|
||||
throw new Error('Cannot save an empty store')
|
||||
}
|
||||
// Scrub undefined values
|
||||
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
||||
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
||||
@@ -183,10 +216,10 @@ export class ConfigService {
|
||||
/**
|
||||
* Writes config YAML as string
|
||||
*/
|
||||
writeRaw (data: string): void {
|
||||
async writeRaw (data: string): Promise<void> {
|
||||
this._store = yaml.load(data)
|
||||
this.save()
|
||||
this.load()
|
||||
await this.save()
|
||||
await this.load()
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
@@ -208,7 +241,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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -228,8 +261,8 @@ export class ConfigService {
|
||||
this.ready.next(true)
|
||||
this.ready.complete()
|
||||
|
||||
this.hostApp.configChangeBroadcast$.subscribe(() => {
|
||||
this.load()
|
||||
this.hostApp.configChangeBroadcast$.subscribe(async () => {
|
||||
await this.load()
|
||||
this.emitChange()
|
||||
})
|
||||
}
|
||||
@@ -250,6 +283,67 @@ export class ConfigService {
|
||||
}
|
||||
config.version = 1
|
||||
}
|
||||
if (config.version < 2) {
|
||||
config.profiles ??= []
|
||||
if (config.terminal?.recoverTabs !== undefined) {
|
||||
config.recoverTabs = config.terminal.recoverTabs
|
||||
delete config.terminal.recoverTabs
|
||||
}
|
||||
for (const profile of config.terminal?.profiles ?? []) {
|
||||
if (profile.sessionOptions) {
|
||||
profile.options = profile.sessionOptions
|
||||
delete profile.sessionOptions
|
||||
}
|
||||
profile.type = 'local'
|
||||
profile.id = `local:custom:${uuidv4()}`
|
||||
}
|
||||
if (config.terminal?.profiles) {
|
||||
config.profiles = config.terminal.profiles
|
||||
delete config.terminal.profiles
|
||||
delete config.terminal.environment
|
||||
config.terminal.profile = `local:${config.terminal.profile}`
|
||||
}
|
||||
config.version = 2
|
||||
}
|
||||
if (config.version < 3) {
|
||||
delete config.ssh?.recentConnections
|
||||
for (const c of config.ssh?.connections ?? []) {
|
||||
const p = {
|
||||
id: `ssh:${uuidv4()}`,
|
||||
type: 'ssh',
|
||||
icon: 'fas fa-desktop',
|
||||
name: c.name,
|
||||
group: c.group ?? undefined,
|
||||
color: c.color,
|
||||
disableDynamicTitle: c.disableDynamicTitle,
|
||||
options: c,
|
||||
}
|
||||
config.profiles.push(p)
|
||||
}
|
||||
for (const p of config.profiles ?? []) {
|
||||
if (p.type === 'ssh') {
|
||||
if (p.options.jumpHost) {
|
||||
p.options.jumpHost = config.profiles.find(x => x.name === p.options.jumpHost)?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const c of config.serial?.connections ?? []) {
|
||||
const p = {
|
||||
id: `serial:${uuidv4()}`,
|
||||
type: 'serial',
|
||||
icon: 'fas fa-microchip',
|
||||
name: c.name,
|
||||
group: c.group ?? undefined,
|
||||
color: c.color,
|
||||
options: c,
|
||||
}
|
||||
config.profiles.push(p)
|
||||
}
|
||||
delete config.ssh?.connections
|
||||
delete config.serial?.connections
|
||||
delete window.localStorage.lastSerialConnection
|
||||
config.version = 3
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeDecryptConfig (store) {
|
||||
@@ -281,6 +375,7 @@ export class ConfigService {
|
||||
detail: e.toString(),
|
||||
buttons: ['Erase config', 'Quit'],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
})
|
||||
if (result.response === 1) {
|
||||
this.platform.quit()
|
||||
@@ -291,10 +386,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,9 +406,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import * as mixpanel from 'mixpanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ConfigService } from './config.service'
|
||||
import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
|
||||
import { PlatformService, BOOTSTRAP_DATA, BootstrapData, HostAppService } from '../api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HomeBaseService {
|
||||
@@ -13,6 +13,7 @@ export class HomeBaseService {
|
||||
private constructor (
|
||||
private config: ConfigService,
|
||||
private platform: PlatformService,
|
||||
private hostApp: HostAppService,
|
||||
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
|
||||
) {
|
||||
this.appVersion = platform.getAppVersion()
|
||||
@@ -28,20 +29,11 @@ export class HomeBaseService {
|
||||
|
||||
reportBug (): void {
|
||||
let body = `Version: ${this.appVersion}\n`
|
||||
body += `Platform: ${process.platform} ${this.platform.getOSRelease()}\n`
|
||||
const label = {
|
||||
aix: 'OS: IBM AIX',
|
||||
android: 'OS: Android',
|
||||
darwin: 'OS: macOS',
|
||||
freebsd: 'OS: FreeBSD',
|
||||
linux: 'OS: Linux',
|
||||
openbsd: 'OS: OpenBSD',
|
||||
sunos: 'OS: Solaris',
|
||||
win32: 'OS: Windows',
|
||||
}[process.platform]
|
||||
body += `Platform: ${this.hostApp.platform} ${process.arch} ${this.platform.getOSRelease()}\n`
|
||||
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
||||
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
|
||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n`
|
||||
body += `Frontend: ${this.config.store.terminal?.frontend}\n\n`
|
||||
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}`)
|
||||
}
|
||||
|
||||
enableAnalytics (): void {
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||
import { stringifyKeySequence, EventData } from './hotkeys.util'
|
||||
import { KeyEventData, getKeyName, Keystroke, KeyName, getKeystrokeName, metaKeyName, altKeyName } from './hotkeys.util'
|
||||
import { ConfigService } from './config.service'
|
||||
import { HostAppService, Platform } from '../api/hostApp'
|
||||
import { deprecate } from 'util'
|
||||
|
||||
export interface PartialHotkeyMatch {
|
||||
id: string
|
||||
@@ -10,14 +12,17 @@ export interface PartialHotkeyMatch {
|
||||
matchedLength: number
|
||||
}
|
||||
|
||||
const KEY_TIMEOUT = 2000
|
||||
|
||||
interface PastKeystroke {
|
||||
keystroke: Keystroke
|
||||
time: number
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HotkeysService {
|
||||
/** @hidden @deprecated */
|
||||
key = new EventEmitter<KeyboardEvent>()
|
||||
|
||||
/** @hidden */
|
||||
/** @hidden @deprecated */
|
||||
matchedHotkey = new EventEmitter<string>()
|
||||
|
||||
/**
|
||||
@@ -25,130 +30,214 @@ export class HotkeysService {
|
||||
*/
|
||||
get hotkey$ (): Observable<string> { return this._hotkey }
|
||||
|
||||
/**
|
||||
* Fired for once hotkey is released
|
||||
*/
|
||||
get hotkeyOff$ (): Observable<string> { return this._hotkeyOff }
|
||||
|
||||
/**
|
||||
* Fired for each singular key
|
||||
*/
|
||||
get key$ (): Observable<KeyName> { return this._key }
|
||||
|
||||
/**
|
||||
* Fired for each key event
|
||||
*/
|
||||
get keyEvent$ (): Observable<KeyboardEvent> { return this._keyEvent }
|
||||
|
||||
/**
|
||||
* Fired for each singular key combination
|
||||
*/
|
||||
get keystroke$ (): Observable<Keystroke> { return this._keystroke }
|
||||
|
||||
private _hotkey = new Subject<string>()
|
||||
private currentKeystrokes: EventData[] = []
|
||||
private _hotkeyOff = new Subject<string>()
|
||||
private _keyEvent = new Subject<KeyboardEvent>()
|
||||
private _key = new Subject<KeyName>()
|
||||
private _keystroke = new Subject<Keystroke>()
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||
|
||||
private pressedKeys = new Set<KeyName>()
|
||||
private pressedKeyTimestamps = new Map<KeyName, number>()
|
||||
private pressedHotkey: string|null = null
|
||||
private pressedKeystroke: Keystroke|null = null
|
||||
private lastKeystrokes: PastKeystroke[] = []
|
||||
private recognitionPhase = true
|
||||
private lastEventTimestamp = 0
|
||||
|
||||
private constructor (
|
||||
private zone: NgZone,
|
||||
private config: ConfigService,
|
||||
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
|
||||
hostApp: HostAppService,
|
||||
) {
|
||||
const events = ['keydown', 'keyup']
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, (nativeEvent: KeyboardEvent) => {
|
||||
if (document.querySelectorAll('input:focus').length === 0) {
|
||||
this.pushKeystroke(event, nativeEvent)
|
||||
this.processKeystrokes()
|
||||
this.emitKeyEvent(nativeEvent)
|
||||
}
|
||||
})
|
||||
})
|
||||
this.config.ready$.toPromise().then(() => {
|
||||
this.getHotkeyDescriptions().then(hotkeys => {
|
||||
this.hotkeyDescriptions = hotkeys
|
||||
this.config.ready$.toPromise().then(async () => {
|
||||
const hotkeys = await this.getHotkeyDescriptions()
|
||||
this.hotkeyDescriptions = hotkeys
|
||||
const events = ['keydown', 'keyup']
|
||||
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
|
||||
this._keyEvent.next(nativeEvent)
|
||||
this.pushKeyEvent(eventType, nativeEvent)
|
||||
if (hostApp.platform === Platform.Web && this.matchActiveHotkey(true) !== null) {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// deprecated
|
||||
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
||||
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
|
||||
this.keyEvent$.subscribe(h => this.key.next(h))
|
||||
this.key.subscribe = deprecate(s => this.keyEvent$.subscribe(s), 'key is deprecated, use keyEvent$')
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new key event to the buffer
|
||||
*
|
||||
* @param name DOM event name
|
||||
* @param eventName DOM event name
|
||||
* @param nativeEvent event object
|
||||
*/
|
||||
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
|
||||
(nativeEvent as any).event = name
|
||||
this.currentKeystrokes.push({
|
||||
pushKeyEvent (eventName: string, nativeEvent: KeyboardEvent): void {
|
||||
if (nativeEvent.timeStamp === this.lastEventTimestamp) {
|
||||
return
|
||||
}
|
||||
|
||||
nativeEvent['event'] = eventName
|
||||
|
||||
const eventData = {
|
||||
ctrlKey: nativeEvent.ctrlKey,
|
||||
metaKey: nativeEvent.metaKey,
|
||||
altKey: nativeEvent.altKey,
|
||||
shiftKey: nativeEvent.shiftKey,
|
||||
code: nativeEvent.code,
|
||||
key: nativeEvent.key,
|
||||
eventName: name,
|
||||
time: performance.now(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the buffer for new complete keystrokes
|
||||
*/
|
||||
processKeystrokes (): void {
|
||||
if (this.isEnabled()) {
|
||||
this.zone.run(() => {
|
||||
const matched = this.getCurrentFullyMatchedHotkey()
|
||||
if (matched) {
|
||||
console.log('Matched hotkey', matched)
|
||||
this._hotkey.next(matched)
|
||||
this.clearCurrentKeystrokes()
|
||||
}
|
||||
})
|
||||
eventName,
|
||||
time: nativeEvent.timeStamp,
|
||||
registrationTime: performance.now(),
|
||||
}
|
||||
|
||||
for (const [key, time] of this.pressedKeyTimestamps.entries()) {
|
||||
if (time < performance.now() - 2000) {
|
||||
this.removePressedKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
const keyName = getKeyName(eventData)
|
||||
if (eventName === 'keydown') {
|
||||
this.addPressedKey(keyName, eventData)
|
||||
if (!nativeEvent.repeat) {
|
||||
this.recognitionPhase = true
|
||||
}
|
||||
this.updateModifiers(eventData)
|
||||
}
|
||||
if (eventName === 'keyup') {
|
||||
const keystroke = getKeystrokeName([...this.pressedKeys])
|
||||
if (this.recognitionPhase) {
|
||||
this._keystroke.next(keystroke)
|
||||
this.lastKeystrokes.push({
|
||||
keystroke,
|
||||
time: performance.now(),
|
||||
})
|
||||
this.recognitionPhase = false
|
||||
}
|
||||
this.removePressedKey(keyName)
|
||||
}
|
||||
|
||||
if (this.pressedKeys.size) {
|
||||
this.pressedKeystroke = getKeystrokeName([...this.pressedKeys])
|
||||
} else {
|
||||
this.pressedKeystroke = null
|
||||
}
|
||||
|
||||
const matched = this.matchActiveHotkey()
|
||||
this.zone.run(() => {
|
||||
if (matched && this.recognitionPhase) {
|
||||
this.emitHotkeyOn(matched)
|
||||
} else if (this.pressedHotkey) {
|
||||
this.emitHotkeyOff(this.pressedHotkey)
|
||||
}
|
||||
})
|
||||
|
||||
this.zone.run(() => {
|
||||
this._key.next(getKeyName(eventData))
|
||||
})
|
||||
|
||||
if (process.platform === 'darwin' && eventData.metaKey && eventName === 'keydown' && !['Ctrl', 'Shift', altKeyName, metaKeyName].includes(keyName)) {
|
||||
// macOS will swallow non-modified keyups if Cmd is held down
|
||||
this.pushKeyEvent('keyup', nativeEvent)
|
||||
}
|
||||
|
||||
this.lastEventTimestamp = nativeEvent.timeStamp
|
||||
}
|
||||
|
||||
emitKeyEvent (nativeEvent: KeyboardEvent): void {
|
||||
this.zone.run(() => {
|
||||
this.key.emit(nativeEvent)
|
||||
})
|
||||
getCurrentKeystrokes (): Keystroke[] {
|
||||
if (!this.pressedKeystroke) {
|
||||
return []
|
||||
}
|
||||
return [...this.lastKeystrokes.map(x => x.keystroke), this.pressedKeystroke]
|
||||
}
|
||||
|
||||
matchActiveHotkey (partial = false): string|null {
|
||||
if (!this.isEnabled() || !this.pressedKeystroke) {
|
||||
return null
|
||||
}
|
||||
const matches: {
|
||||
id: string,
|
||||
sequence: string[],
|
||||
}[] = []
|
||||
|
||||
const currentSequence = this.getCurrentKeystrokes()
|
||||
|
||||
const config = this.getHotkeysConfig()
|
||||
for (const id in config) {
|
||||
for (const sequence of config[id]) {
|
||||
if (currentSequence.length < sequence.length) {
|
||||
continue
|
||||
}
|
||||
if (sequence[sequence.length - 1] !== this.pressedKeystroke) {
|
||||
continue
|
||||
}
|
||||
|
||||
let lastIndex = 0
|
||||
let matched = true
|
||||
for (const item of sequence) {
|
||||
const nextOffset = currentSequence.slice(lastIndex).findIndex(
|
||||
x => x.toLowerCase() === item.toLowerCase()
|
||||
)
|
||||
if (nextOffset === -1) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
lastIndex += nextOffset
|
||||
}
|
||||
|
||||
if (partial ? lastIndex > 0 : matched) {
|
||||
matches.push({
|
||||
id,
|
||||
sequence,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((a, b) => b.sequence.length - a.sequence.length)
|
||||
if (!matches.length) {
|
||||
return null
|
||||
}
|
||||
return matches[0].id
|
||||
}
|
||||
|
||||
clearCurrentKeystrokes (): void {
|
||||
this.currentKeystrokes = []
|
||||
}
|
||||
|
||||
getCurrentKeystrokes (): string[] {
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
|
||||
return stringifyKeySequence(this.currentKeystrokes)
|
||||
}
|
||||
|
||||
getCurrentFullyMatchedHotkey (): string|null {
|
||||
const currentStrokes = this.getCurrentKeystrokes()
|
||||
const config = this.getHotkeysConfig()
|
||||
for (const id in config) {
|
||||
for (const sequence of config[id]) {
|
||||
if (currentStrokes.length < sequence.length) {
|
||||
continue
|
||||
}
|
||||
if (sequence.every(
|
||||
(x: string, index: number) =>
|
||||
x.toLowerCase() ===
|
||||
currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase()
|
||||
)) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] {
|
||||
const currentStrokes = this.getCurrentKeystrokes()
|
||||
const config = this.getHotkeysConfig()
|
||||
const result: PartialHotkeyMatch[] = []
|
||||
for (const id in config) {
|
||||
for (const sequence of config[id]) {
|
||||
for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) {
|
||||
if (sequence.slice(0, matchLength).every(
|
||||
(x: string, index: number) =>
|
||||
x.toLowerCase() ===
|
||||
currentStrokes[currentStrokes.length - matchLength + index].toLowerCase()
|
||||
)) {
|
||||
result.push({
|
||||
matchedLength: matchLength,
|
||||
id,
|
||||
strokes: sequence,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
this.lastKeystrokes = []
|
||||
this.pressedKeys.clear()
|
||||
this.pressedKeyTimestamps.clear()
|
||||
this.pressedKeystroke = null
|
||||
this.pressedHotkey = null
|
||||
}
|
||||
|
||||
getHotkeyDescription (id: string): HotkeyDescription {
|
||||
@@ -176,6 +265,42 @@ export class HotkeysService {
|
||||
).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
private updateModifiers (event: KeyEventData) {
|
||||
for (const [prop, key] of Object.entries({
|
||||
ctrlKey: 'Ctrl',
|
||||
metaKey: metaKeyName,
|
||||
altKey: altKeyName,
|
||||
shiftKey: 'Shift',
|
||||
})) {
|
||||
if (!event[prop] && this.pressedKeys.has(key)) {
|
||||
this.removePressedKey(key)
|
||||
}
|
||||
if (event[prop] && !this.pressedKeys.has(key)) {
|
||||
this.addPressedKey(key, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitHotkeyOn (hotkey: string) {
|
||||
if (this.pressedHotkey) {
|
||||
if (this.pressedHotkey === hotkey) {
|
||||
return
|
||||
}
|
||||
this.emitHotkeyOff(this.pressedHotkey)
|
||||
}
|
||||
if (document.querySelectorAll('input:focus').length === 0) {
|
||||
console.debug('Matched hotkey', hotkey)
|
||||
this._hotkey.next(hotkey)
|
||||
this.pressedHotkey = hotkey
|
||||
}
|
||||
}
|
||||
|
||||
private emitHotkeyOff (hotkey: string) {
|
||||
console.debug('Unmatched hotkey', hotkey)
|
||||
this._hotkeyOff.next(hotkey)
|
||||
this.pressedHotkey = null
|
||||
}
|
||||
|
||||
private getHotkeysConfig () {
|
||||
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
||||
}
|
||||
@@ -204,4 +329,14 @@ export class HotkeysService {
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
private addPressedKey (keyName: KeyName, eventData: KeyEventData) {
|
||||
this.pressedKeys.add(keyName)
|
||||
this.pressedKeyTimestamps.set(keyName, eventData.registrationTime)
|
||||
}
|
||||
|
||||
private removePressedKey (key: KeyName) {
|
||||
this.pressedKeys.delete(key)
|
||||
this.pressedKeyTimestamps.delete(key)
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-type-alias */
|
||||
export const metaKeyName = {
|
||||
darwin: '⌘',
|
||||
win32: 'Win',
|
||||
@@ -10,72 +11,67 @@ export const altKeyName = {
|
||||
linux: 'Alt',
|
||||
}[process.platform]
|
||||
|
||||
export interface EventData {
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
shiftKey: boolean
|
||||
export interface KeyEventData {
|
||||
ctrlKey?: boolean
|
||||
metaKey?: boolean
|
||||
altKey?: boolean
|
||||
shiftKey?: boolean
|
||||
key: string
|
||||
code: string
|
||||
eventName: string
|
||||
time: number
|
||||
registrationTime: number
|
||||
}
|
||||
|
||||
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
|
||||
|
||||
export function stringifyKeySequence (events: EventData[]): string[] {
|
||||
const items: string[] = []
|
||||
events = events.slice()
|
||||
export type KeyName = string
|
||||
export type Keystroke = string
|
||||
|
||||
while (events.length > 0) {
|
||||
const event = events.shift()!
|
||||
if (event.eventName === 'keydown') {
|
||||
const itemKeys: string[] = []
|
||||
if (event.ctrlKey) {
|
||||
itemKeys.push('Ctrl')
|
||||
}
|
||||
if (event.metaKey) {
|
||||
itemKeys.push(metaKeyName)
|
||||
}
|
||||
if (event.altKey) {
|
||||
itemKeys.push(altKeyName)
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
itemKeys.push('Shift')
|
||||
}
|
||||
|
||||
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
|
||||
// TODO make this optional?
|
||||
continue
|
||||
}
|
||||
|
||||
let key = event.code
|
||||
if (REGEX_LATIN_KEYNAME.test(event.key)) {
|
||||
// Handle Dvorak etc via the reported "character" instead of the scancode
|
||||
key = event.key.toUpperCase()
|
||||
} else {
|
||||
key = key.replace('Key', '')
|
||||
key = key.replace('Arrow', '')
|
||||
key = key.replace('Digit', '')
|
||||
key = {
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
Backslash: '\\',
|
||||
IntlBackslash: '`',
|
||||
Backquote: '~', // Electron says it's the tilde
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
}[key] ?? key
|
||||
}
|
||||
|
||||
itemKeys.push(key)
|
||||
items.push(itemKeys.join('-'))
|
||||
export function getKeyName (event: KeyEventData): KeyName {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||
let key: string
|
||||
if (event.key === 'Control') {
|
||||
key = 'Ctrl'
|
||||
} else if (event.key === 'Meta') {
|
||||
key = metaKeyName
|
||||
} else if (event.key === 'Alt') {
|
||||
key = altKeyName
|
||||
} else if (event.key === 'Shift') {
|
||||
key = 'Shift'
|
||||
} else {
|
||||
key = event.code
|
||||
if (REGEX_LATIN_KEYNAME.test(event.key)) {
|
||||
// Handle Dvorak etc via the reported "character" instead of the scancode
|
||||
key = event.key.toUpperCase()
|
||||
} else {
|
||||
key = key.replace('Key', '')
|
||||
key = key.replace('Arrow', '')
|
||||
key = key.replace('Digit', '')
|
||||
key = {
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
Backslash: '\\',
|
||||
IntlBackslash: '`',
|
||||
Backquote: '~', // Electron says it's the tilde
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
}[key] ?? key
|
||||
}
|
||||
}
|
||||
return items
|
||||
return key
|
||||
}
|
||||
|
||||
export function getKeystrokeName (keys: KeyName[]): Keystroke {
|
||||
const strictOrdering: KeyName[] = ['Ctrl', metaKeyName, altKeyName, 'Shift']
|
||||
keys = [
|
||||
...strictOrdering.map(x => keys.find(k => k === x)).filter(x => !!x) as KeyName[],
|
||||
...keys.filter(k => !strictOrdering.includes(k)),
|
||||
]
|
||||
return keys.join('-')
|
||||
}
|
||||
|
191
tabby-core/src/services/profiles.service.ts
Normal file
191
tabby-core/src/services/profiles.service.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { NewTabParameters } from './tabs.service'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
|
||||
import { SelectorOption } from '../api/selector'
|
||||
import { AppService } from './app.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,
|
||||
private notifications: NotificationsService,
|
||||
private selector: SelectorService,
|
||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
||||
) { }
|
||||
|
||||
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
|
||||
|
||||
if (profile.name) {
|
||||
tab.setTitle(profile.name)
|
||||
}
|
||||
if (profile.disableDynamicTitle) {
|
||||
tab['disableDynamicTitle'] = true
|
||||
}
|
||||
return tab
|
||||
}
|
||||
return 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<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
}
|
||||
|
||||
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,
|
||||
]
|
||||
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 <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
|
||||
}
|
||||
|
||||
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 {
|
||||
...profile,
|
||||
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>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
|
||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
group: 'Recent',
|
||||
icon: 'fas fa-history',
|
||||
color: p.color,
|
||||
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 profiles',
|
||||
group: 'Recent',
|
||||
icon: 'fas fa-eraser',
|
||||
callback: async () => {
|
||||
window.localStorage.removeItem('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>, skipUserDefaults = false): T {
|
||||
const provider = this.providerForProfile(profile)
|
||||
const defaults = [
|
||||
this.profileDefaults,
|
||||
provider?.configDefaults ?? {},
|
||||
!provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
|
||||
].reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
}
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
|
||||
import { TabRecoveryProvider, RecoveryToken } from '../api/tabRecovery'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { NewTabParameters } from './tabs.service'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -11,7 +12,7 @@ export class TabRecoveryService {
|
||||
enabled = false
|
||||
|
||||
private constructor (
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[]|null,
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider<BaseTabComponent>[]|null,
|
||||
private config: ConfigService,
|
||||
log: LogService
|
||||
) {
|
||||
@@ -19,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(
|
||||
@@ -33,14 +34,16 @@ export class TabRecoveryService {
|
||||
const token = await tab.getRecoveryToken()
|
||||
if (token) {
|
||||
token.tabTitle = tab.title
|
||||
token.tabCustomTitle = tab.customTitle
|
||||
if (tab.color) {
|
||||
token.tabColor = tab.color
|
||||
}
|
||||
token.disableDynamicTitle = tab['disableDynamicTitle']
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
async recoverTab (token: RecoveryToken, duplicate = false): Promise<RecoveredTab|null> {
|
||||
async recoverTab (token: RecoveryToken, duplicate = false): Promise<NewTabParameters<BaseTabComponent>|null> {
|
||||
for (const provider of this.config.enabledServices(this.tabRecoveryProviders ?? [])) {
|
||||
try {
|
||||
if (!await provider.applicableTo(token)) {
|
||||
@@ -50,9 +53,11 @@ export class TabRecoveryService {
|
||||
token = provider.duplicate(token)
|
||||
}
|
||||
const tab = await provider.recover(token)
|
||||
tab.options = tab.options || {}
|
||||
tab.options.color = token.tabColor ?? null
|
||||
tab.options.title = token.tabTitle || ''
|
||||
tab.inputs = tab.inputs ?? {}
|
||||
tab.inputs.color = token.tabColor ?? null
|
||||
tab.inputs.title = token.tabTitle || ''
|
||||
tab.inputs.customTitle = token.tabCustomTitle || ''
|
||||
tab.inputs.disableDynamicTitle = token.disableDynamicTitle
|
||||
return tab
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
@@ -61,9 +66,9 @@ export class TabRecoveryService {
|
||||
return null
|
||||
}
|
||||
|
||||
async recoverTabs (): Promise<RecoveredTab[]> {
|
||||
async recoverTabs (): Promise<NewTabParameters<BaseTabComponent>[]> {
|
||||
if (window.localStorage.tabsRecovery) {
|
||||
const tabs: RecoveredTab[] = []
|
||||
const tabs: NewTabParameters<BaseTabComponent>[] = []
|
||||
for (const token of JSON.parse(window.localStorage.tabsRecovery)) {
|
||||
const tab = await this.recoverTab(token)
|
||||
if (tab) {
|
||||
|
@@ -2,8 +2,22 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabRecoveryService } from './tabRecovery.service'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
export type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
export interface TabComponentType<T extends BaseTabComponent> {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||
new (...args: any[]): T
|
||||
}
|
||||
|
||||
export interface NewTabParameters<T extends BaseTabComponent> {
|
||||
/**
|
||||
* Component type to be instantiated
|
||||
*/
|
||||
type: TabComponentType<T>
|
||||
|
||||
/**
|
||||
* Component instance inputs
|
||||
*/
|
||||
inputs?: Record<string, any>
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TabsService {
|
||||
@@ -17,12 +31,12 @@ export class TabsService {
|
||||
/**
|
||||
* Instantiates a tab component and assigns given inputs
|
||||
*/
|
||||
create (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
||||
create <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(params.type)
|
||||
const componentRef = componentFactory.create(this.injector)
|
||||
const tab = componentRef.instance
|
||||
tab.hostView = componentRef.hostView
|
||||
Object.assign(tab, inputs ?? {})
|
||||
Object.assign(tab, params.inputs ?? {})
|
||||
return tab
|
||||
}
|
||||
|
||||
@@ -36,7 +50,7 @@ export class TabsService {
|
||||
}
|
||||
const dup = await this.tabRecovery.recoverTab(token, true)
|
||||
if (dup) {
|
||||
return this.create(dup.type, dup.options)
|
||||
return this.create(dup)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@@ -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,17 +270,17 @@ 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: 'plus',
|
||||
icon: 'fas fa-plus',
|
||||
result: null,
|
||||
},
|
||||
...files.map(f => ({
|
||||
name: f.key.description,
|
||||
icon: 'file',
|
||||
icon: 'fas fa-file',
|
||||
result: f,
|
||||
})),
|
||||
])
|
||||
@@ -270,11 +298,11 @@ export class VaultFileProvider extends FileProvider {
|
||||
}
|
||||
const transfer = transfers[0]
|
||||
const id = (await wrapPromise(this.zone, promisify(crypto.randomBytes)(32))).toString('hex')
|
||||
this.vault.addSecret({
|
||||
await this.vault.addSecret({
|
||||
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,12 @@ 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'
|
||||
import { TAB_COLORS } from './utils'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -83,16 +90,6 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
@@ -100,6 +97,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private ngbModal: NgbModal,
|
||||
private splitLayoutProfilesService: SplitLayoutProfilesService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -119,8 +118,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
},
|
||||
{
|
||||
label: 'Color',
|
||||
sublabel: COLORS.find(x => x.value === tab.color)?.name,
|
||||
submenu: COLORS.map(color => ({
|
||||
sublabel: TAB_COLORS.find(x => x.value === tab.color)?.name,
|
||||
submenu: TAB_COLORS.map(color => ({
|
||||
label: color.name,
|
||||
type: 'radio',
|
||||
checked: tab.color === color.value,
|
||||
@@ -130,6 +129,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 +217,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;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user