mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-31 22:46:58 +00:00
Compare commits
255 Commits
dependabot
...
all-contri
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7f8652f5f | ||
![]() |
1f06800c39 | ||
![]() |
7239193a4b | ||
![]() |
35cb81c854 | ||
![]() |
614c1adf96 | ||
![]() |
eb15fe6aa6 | ||
![]() |
afe716fcf4 | ||
![]() |
2e50c18dba | ||
![]() |
209d3a9c5b | ||
![]() |
efc462ee4e | ||
![]() |
7b4af46bd4 | ||
![]() |
c9339ea76a | ||
![]() |
ac95f550d9 | ||
![]() |
cc3af1aa03 | ||
![]() |
a6ac5a2898 | ||
![]() |
353e5a9550 | ||
![]() |
2507317d6b | ||
![]() |
78a64a9025 | ||
![]() |
1369b5b299 | ||
![]() |
76931e69ad | ||
![]() |
e4ceb3ee6a | ||
![]() |
86196c9f7d | ||
![]() |
62e54cbf6e | ||
![]() |
cd8e414fc0 | ||
![]() |
60be3722a2 | ||
![]() |
4e4ee4fc98 | ||
![]() |
d3b5a53179 | ||
![]() |
2a51ce556f | ||
![]() |
62dc3e04d0 | ||
![]() |
db9d90e9b0 | ||
![]() |
86469334a0 | ||
![]() |
eec294fa2e | ||
![]() |
aac1976baa | ||
![]() |
a4aa07c1f1 | ||
![]() |
2437dc5bdc | ||
![]() |
a8ef5963c3 | ||
![]() |
2dc64ae51b | ||
![]() |
0ba5517a31 | ||
![]() |
9f6263f3d4 | ||
![]() |
f80db81857 | ||
![]() |
3794081cef | ||
![]() |
38c9714c75 | ||
![]() |
3823efc82a | ||
![]() |
977deea2a0 | ||
![]() |
0153bdf490 | ||
![]() |
0b608bdb24 | ||
![]() |
8180530892 | ||
![]() |
0e58bb28f9 | ||
![]() |
7908de9634 | ||
![]() |
2743c332d5 | ||
![]() |
1e8693a47d | ||
![]() |
c97f88fde6 | ||
![]() |
b97d73e912 | ||
![]() |
e16cc34b4e | ||
![]() |
a2b87ea8cb | ||
![]() |
fda98f7434 | ||
![]() |
aae0147d7d | ||
![]() |
c59e9c1d82 | ||
![]() |
b4abe29c51 | ||
![]() |
1e096ede77 | ||
![]() |
2be079a51b | ||
![]() |
be0d129c28 | ||
![]() |
191807c09a | ||
![]() |
f4af21bf71 | ||
![]() |
845d6c6373 | ||
![]() |
066a1a6bac | ||
![]() |
b4777fd4a8 | ||
![]() |
3d83bc6e4e | ||
![]() |
102c0f5ce7 | ||
![]() |
345e90c1f6 | ||
![]() |
787936babd | ||
![]() |
6c1b0c3ae7 | ||
![]() |
efe1772e8d | ||
![]() |
d1311ba53f | ||
![]() |
5a6911f928 | ||
![]() |
2254436365 | ||
![]() |
2c70c0fff5 | ||
![]() |
ff450527ab | ||
![]() |
3ba6ce889e | ||
![]() |
4ca425c2a8 | ||
![]() |
70fb882e4d | ||
![]() |
86664c4eba | ||
![]() |
72149d2cb0 | ||
![]() |
04a1b0fb44 | ||
![]() |
f8614dffbc | ||
![]() |
37ccba5c90 | ||
![]() |
98719aafdc | ||
![]() |
860ee3eaa9 | ||
![]() |
454391d31e | ||
![]() |
4c0231cd67 | ||
![]() |
31b063a964 | ||
![]() |
26a2a0c44e | ||
![]() |
f0a84d4d76 | ||
![]() |
ae6c1308a8 | ||
![]() |
c3a9f35953 | ||
![]() |
dc6c0357c2 | ||
![]() |
f706d7d6cb | ||
![]() |
56d5d34bfd | ||
![]() |
40246a409d | ||
![]() |
0f6561e859 | ||
![]() |
72c0692843 | ||
![]() |
26d6d14703 | ||
![]() |
6a458d8b9b | ||
![]() |
3e9ee5b235 | ||
![]() |
4740824dd1 | ||
![]() |
51d48f07d7 | ||
![]() |
a8d7fa33b5 | ||
![]() |
34fee58a7d | ||
![]() |
f276c50326 | ||
![]() |
ca1241ee52 | ||
![]() |
000bdde0cd | ||
![]() |
34daa4c412 | ||
![]() |
39596588bb | ||
![]() |
1228b1a29a | ||
![]() |
f0636623ae | ||
![]() |
4057e0cf59 | ||
![]() |
0e8164fe4c | ||
![]() |
2b8fed164f | ||
![]() |
e68bd6c746 | ||
![]() |
f3196ac045 | ||
![]() |
57f20eca4f | ||
![]() |
5d1a35a285 | ||
![]() |
642db6a14a | ||
![]() |
e739883840 | ||
![]() |
bb27314696 | ||
![]() |
adeb6031dd | ||
![]() |
34aeb797f3 | ||
![]() |
7d5128a8ce | ||
![]() |
83c0892546 | ||
![]() |
9e705fb273 | ||
![]() |
c2b349de8f | ||
![]() |
3176a7744b | ||
![]() |
98dd4a0017 | ||
![]() |
cfce2687bd | ||
![]() |
2af7ad85f8 | ||
![]() |
8616bdbea3 | ||
![]() |
0505e5615d | ||
![]() |
8c2820056a | ||
![]() |
b1108a7b32 | ||
![]() |
bfe3430e9a | ||
![]() |
612d6ef089 | ||
![]() |
bd31db9c56 | ||
![]() |
d1a837b11d | ||
![]() |
d70be3abf9 | ||
![]() |
422df52fea | ||
![]() |
140d7e9f96 | ||
![]() |
9a3bb1ae4b | ||
![]() |
169adedc48 | ||
![]() |
90ccd969f2 | ||
![]() |
6ebfccda28 | ||
![]() |
27bf7c4ad6 | ||
![]() |
651861cd4a | ||
![]() |
3ce2bb68c6 | ||
![]() |
e4169a54aa | ||
![]() |
0d0bde82a0 | ||
![]() |
04b53ab366 | ||
![]() |
7cde5c0807 | ||
![]() |
cb71c79ecc | ||
![]() |
6843b87493 | ||
![]() |
a17da4b4af | ||
![]() |
5d5c545b4e | ||
![]() |
f7299ee321 | ||
![]() |
d1e97bf737 | ||
![]() |
f9f1a809a1 | ||
![]() |
d881da94de | ||
![]() |
3aaa68fe09 | ||
![]() |
ea84612788 | ||
![]() |
16fcf3d7f8 | ||
![]() |
4a02abdcc3 | ||
![]() |
98a5bfd338 | ||
![]() |
2dbc43f828 | ||
![]() |
0c93010da7 | ||
![]() |
0329e7a8b6 | ||
![]() |
26ac6b0aa3 | ||
![]() |
8bc2375aab | ||
![]() |
dc9a7d8fac | ||
![]() |
0becf8cc76 | ||
![]() |
6042a5290c | ||
![]() |
344244297c | ||
![]() |
75eedafbc9 | ||
![]() |
1b0ce6d684 | ||
![]() |
4684b0d6f5 | ||
![]() |
eddb50b529 | ||
![]() |
96eee51590 | ||
![]() |
f963167b70 | ||
![]() |
0128013308 | ||
![]() |
5e5c80832d | ||
![]() |
06859de2de | ||
![]() |
49f9a10372 | ||
![]() |
85d988f6b3 | ||
![]() |
7bc549b555 | ||
![]() |
d6bdecb9c4 | ||
![]() |
7687972e65 | ||
![]() |
34786b1459 | ||
![]() |
634d88d220 | ||
![]() |
ad3b03cb83 | ||
![]() |
d354520910 | ||
![]() |
a9c63b5305 | ||
![]() |
d21282501f | ||
![]() |
d2752382aa | ||
![]() |
5eeaef954c | ||
![]() |
ef6b8a4eaa | ||
![]() |
7be6fca493 | ||
![]() |
21e38c8453 | ||
![]() |
2262d59866 | ||
![]() |
9b0fbcfc56 | ||
![]() |
8adb9a6806 | ||
![]() |
164d34c543 | ||
![]() |
f369b140c6 | ||
![]() |
c108476262 | ||
![]() |
695c5ba670 | ||
![]() |
e23765045c | ||
![]() |
d6705c20ad | ||
![]() |
79cabb6eda | ||
![]() |
3024e633a9 | ||
![]() |
743ea04d0b | ||
![]() |
6d0a84c94e | ||
![]() |
0ef24ddf1d | ||
![]() |
935c981d2b | ||
![]() |
951c69b31a | ||
![]() |
30936b739e | ||
![]() |
44c449bd4c | ||
![]() |
2cfc64911b | ||
![]() |
240c542665 | ||
![]() |
555d3c8478 | ||
![]() |
aafe510803 | ||
![]() |
217ab641b5 | ||
![]() |
7572f3c26f | ||
![]() |
326d8b3fb1 | ||
![]() |
9b4f1a3a0d | ||
![]() |
0d275595cf | ||
![]() |
7860c73e49 | ||
![]() |
fa9dea0f64 | ||
![]() |
6813fd54cd | ||
![]() |
1c06a510bd | ||
![]() |
a0804cc564 | ||
![]() |
b751e10082 | ||
![]() |
48d4b8e8f8 | ||
![]() |
ef040ee342 | ||
![]() |
8e9156e250 | ||
![]() |
1903ec5995 | ||
![]() |
21df033012 | ||
![]() |
4d146941f4 | ||
![]() |
5ba6bfbd7d | ||
![]() |
f0e2482dd6 | ||
![]() |
5763919d85 | ||
![]() |
c1e03ed532 | ||
![]() |
8a85fcac21 | ||
![]() |
ee4487a517 | ||
![]() |
272b9ee5dc | ||
![]() |
d57757c66c | ||
![]() |
4dedbbc25a | ||
![]() |
91cb9e5c63 | ||
![]() |
e2cc9b98ee | ||
![]() |
a585cf306c |
@@ -1220,6 +1220,60 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "siebsie23",
|
||||
"name": "Sibren",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25083973?v=4",
|
||||
"profile": "https://siebsie23.nl/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nwalser",
|
||||
"name": "Nathaniel Walser",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33339996?v=4",
|
||||
"profile": "https://www.nathaniel-walser.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aaronhuggins",
|
||||
"name": "Aaron Huggins",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16567111?v=4",
|
||||
"profile": "https://github.com/aaronhuggins",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zKXDEX",
|
||||
"name": "KDex",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66271780?v=4",
|
||||
"profile": "https://zkxdex.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kimbob13",
|
||||
"name": "ChangHwan Kim",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/26755098?v=4",
|
||||
"profile": "https://github.com/kimbob13",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ashneilson",
|
||||
"name": "Ash Neilson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/35913512?v=4",
|
||||
"profile": "https://github.com/ashneilson",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@@ -1,7 +1,13 @@
|
||||
settings:
|
||||
import/parsers:
|
||||
'@typescript-eslint/parser': ['.ts']
|
||||
import/resolver:
|
||||
typescript: true
|
||||
typescript:
|
||||
project:
|
||||
- tsconfig.json
|
||||
- tabby-*/tsconfig.json
|
||||
node: true
|
||||
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
@@ -28,7 +34,7 @@ overrides:
|
||||
- plugin:import/typescript
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
- 'import'
|
||||
- import
|
||||
rules:
|
||||
'@typescript-eslint/semi':
|
||||
- error
|
||||
@@ -130,6 +136,7 @@ overrides:
|
||||
'@typescript-eslint/naming-convention': off
|
||||
'@typescript-eslint/lines-between-class-members':
|
||||
- error
|
||||
- always
|
||||
- exceptAfterSingleLine: true
|
||||
'@typescript-eslint/dot-notation': off
|
||||
'@typescript-eslint/no-implicit-any-catch': off
|
||||
@@ -152,3 +159,6 @@ overrides:
|
||||
'@typescript-eslint/consistent-generic-constructors': off
|
||||
'keyword-spacing': off
|
||||
'@typescript-eslint/keyword-spacing': off
|
||||
'@typescript-eslint/class-methods-use-this': off
|
||||
'@typescript-eslint/lines-around-comment': off
|
||||
'@typescript-eslint/no-redundant-type-constituents': off # broken
|
||||
|
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@ name: Package-Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
Lint:
|
||||
runs-on: macos-11
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: yarn run lint
|
||||
|
||||
macOS-Build:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
needs: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -47,12 +47,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo -H pip3 install setuptools
|
||||
npm config set python python3
|
||||
sudo npm i -g yarn@1.22.1
|
||||
yarn --network-timeout 1000000
|
||||
env:
|
||||
@@ -110,16 +112,16 @@ jobs:
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
mkdir artifact-pkg
|
||||
mv dist/*.pkg artifact-pkg/
|
||||
mkdir artifact-dmg
|
||||
mv dist/*.dmg artifact-dmg/
|
||||
mkdir artifact-zip
|
||||
mv dist/*.zip artifact-zip/
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload PKG
|
||||
name: Upload DMG
|
||||
with:
|
||||
name: macOS .pkg (${{matrix.arch}})
|
||||
path: artifact-pkg
|
||||
name: macOS .dmg (${{matrix.arch}})
|
||||
path: artifact-dmg
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload ZIP
|
||||
@@ -155,14 +157,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install deps (amd64)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libarchive-tools zsh
|
||||
sudo apt-get install libarchive-tools zsh python3-distutils
|
||||
|
||||
- name: Install npm_modules (amd64)
|
||||
run: |
|
||||
@@ -345,7 +347,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
|
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@v3.6.0
|
||||
uses: actions/setup-node@v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
|
15
.github/workflows/issue-translator.yml
vendored
15
.github/workflows/issue-translator.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: 'issue-translator'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: tomsun28/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
CUSTOM_BOT_NOTE: The translator bot has detected that this issue body's language is not English, and has translated it automatically.
|
@@ -119,6 +119,7 @@ Plugins und Themen können direkt aus der Ansicht "Einstellungen" in Tabby insta
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - ein Beispiel-Plugin, das einen die ganze Zeit nervt
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - ermöglicht das Erstellen eigener Workspace-Profile auf Basis der angegebenen Konfiguration
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - öffnet den Standard-Systembrowser mit einem Text, der aus dem Tabby Tab ausgewählt wurde
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
|
||||
@@ -326,6 +327,14 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -120,6 +120,7 @@ Los plugins y los temas se pueden instalar directamente desde la vista de Config
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - un ejemplo de plugin que te molesta todo el tiempo
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - permite crear perfiles de espacio de trabajo personalizados basados en la configuración dada
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - abre el navegador del sistema por defecto con un texto seleccionado en la pestaña de Tabby's
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
<a name="themes"></a>
|
||||
|
||||
# Temas
|
||||
@@ -328,6 +329,14 @@ Gracias a estas maravillosas personas ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -120,6 +120,7 @@ Tema dan Plugin bisa langsung di install dari Pengaturan di dalam Tabby.
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - suatu contoh plugin yang akan mengganggu anda setiap saat
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - memperbolehkan membuat kustom profil workspace dari konfigurasi yang diberikan
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - membuka browser default dengan text yang dipilih dari Tab Tabby
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
|
||||
@@ -325,6 +326,14 @@ Terima kasih kepada mereka yang telah membantu ([emoji key](https://allcontribut
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -117,6 +117,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - an example plugin which annoys you all the time
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - allows creating custom workspace profiles based on the given config
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - opens default system browser with a text selected from the Tabby's tab
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
# Temi
|
||||
@@ -321,6 +322,14 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -127,6 +127,7 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - プラグインの作例として、いつも厄介なあいつが出てくるプラグイン
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 指定された設定からカスタマイズされたワークスペースを作成することができます
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby内の端末で選択したテキストを既定ブラウザで開くことができます。
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
|
||||
@@ -336,6 +337,14 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -111,6 +111,7 @@
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - 항상 당신을 귀찮게 하는 예제 플러그인
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 주어진 구성을 기반으로 사용자 정의 작업 공간 프로필을 생성할 수 있습니다
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby의 탭에서 선택한 텍스트로 기본 시스템 브라우저를 엽니다
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
# 테마
|
||||
@@ -315,6 +316,14 @@ Pull requests and plugins are welcome!
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
14
README.md
14
README.md
@@ -128,6 +128,10 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - an example plugin which annoys you all the time
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - allows creating custom workspace profiles based on the given config
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - opens default system browser with a text selected from the Tabby's tab
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
* [background](https://github.com/moemoechu/tabby-background) - change Tabby background image and more...
|
||||
* [highlight](https://github.com/moemoechu/tabby-highlight) - Tabby terminal keyword highlight plugin
|
||||
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - In-app web authentication popups (Built primarily for warpgate in-browser auth)
|
||||
|
||||
<a name="themes"></a>
|
||||
|
||||
@@ -138,6 +142,8 @@ Plugins and themes can be installed directly from the Settings view inside 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)
|
||||
* [catppuccin](https://github.com/catppuccin/tabby) - Soothing pastel theme for Tabby
|
||||
* [noctis](https://github.com/aaronhuggins/tabby-colors-noctis) - color themes inspired by Noctis VS Code theme
|
||||
|
||||
# Sponsors <!-- omit in toc -->
|
||||
|
||||
@@ -337,6 +343,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -120,6 +120,7 @@ Plugins e temas podem ser instalados durante a execução na pagina de configura
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - um plugin de exemplo que te incomoda o tempo todo
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - permite criar perfis de espaço de trabalho personalizados com base na configuração fornecida
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - abre o navegador padrão do sistema com um texto selecionado na guia do Tabby
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
|
||||
@@ -329,6 +330,14 @@ Obrigado vai para essas pessoas maravilhosas ([emoji key](https://allcontributor
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -117,6 +117,7 @@
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) — плагин-пример, который постоянно будет вас бесить;
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) — позволяет создавать пользовательские провили рабочего окружеиня на основе конфига;
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) — открывает браузер по умолчанию с текстом, выделенном во вкладке Tabby.
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
|
||||
|
||||
<a name="themes"></a>
|
||||
# Темы
|
||||
@@ -321,6 +322,14 @@ Pull-запросы и плагины приветствуются!
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -116,6 +116,7 @@
|
||||
* [clippy](https://github.com/Eugeny/tabby-clippy) - 一个可以一直烦你的示例插件
|
||||
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 允许根据给定的配置创建自定义工作区配置文件
|
||||
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - 从 Tabby 标签页带有选中的文本来打开系统默认浏览器
|
||||
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - 为ssh连接打开类似SecureCRT的sftp标签页
|
||||
|
||||
<a name="themes"></a>
|
||||
# 主题
|
||||
@@ -320,6 +321,14 @@
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wljince007"><img src="https://avatars.githubusercontent.com/u/88243938?v=4?s=100" width="100px;" alt="wljince007"/><br /><sub><b>wljince007</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=wljince007" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FeroTheFox"><img src="https://avatars.githubusercontent.com/u/52982404?v=4?s=100" width="100px;" alt="fero"/><br /><sub><b>fero</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=FeroTheFox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://siebsie23.nl/"><img src="https://avatars.githubusercontent.com/u/25083973?v=4?s=100" width="100px;" alt="Sibren"/><br /><sub><b>Sibren</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=siebsie23" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.nathaniel-walser.com"><img src="https://avatars.githubusercontent.com/u/33339996?v=4?s=100" width="100px;" alt="Nathaniel Walser"/><br /><sub><b>Nathaniel Walser</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=nwalser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronhuggins"><img src="https://avatars.githubusercontent.com/u/16567111?v=4?s=100" width="100px;" alt="Aaron Huggins"/><br /><sub><b>Aaron Huggins</b></sub></a><br /><a href="#design-aaronhuggins" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zkxdex.github.io/"><img src="https://avatars.githubusercontent.com/u/66271780?v=4?s=100" width="100px;" alt="KDex"/><br /><sub><b>KDex</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=zKXDEX" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kimbob13"><img src="https://avatars.githubusercontent.com/u/26755098?v=4?s=100" width="100px;" alt="ChangHwan Kim"/><br /><sub><b>ChangHwan Kim</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=kimbob13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashneilson"><img src="https://avatars.githubusercontent.com/u/35913512?v=4?s=100" width="100px;" alt="Ash Neilson"/><br /><sub><b>Ash Neilson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=ashneilson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -183,7 +183,7 @@ export class Application {
|
||||
}
|
||||
|
||||
enableTray (): void {
|
||||
if (this.tray || process.platform === 'linux') {
|
||||
if (!!this.tray || process.platform === 'linux') {
|
||||
return
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
|
@@ -28,6 +28,16 @@ export function parseArgs (argv: string[], cwd: string): any {
|
||||
.command('recent [index]', 'open a tab with a recent profile', {
|
||||
profileNumber: { type: 'number' },
|
||||
})
|
||||
.command('quickConnect <providerId> <query>', 'open a tab for specified quick connect provider', yargs => {
|
||||
return yargs.positional('providerId', {
|
||||
describe: 'The name of a quick connect profile provider',
|
||||
type: 'string',
|
||||
choices: ['ssh', 'telnet'],
|
||||
}).positional('query', {
|
||||
describe: 'The quick connect query string',
|
||||
type: 'string',
|
||||
})
|
||||
})
|
||||
.version(app.getVersion())
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { app } from 'electron'
|
||||
import { writeFile } from 'atomically'
|
||||
|
||||
|
||||
export const configPath = path.join(process.env.TABBY_CONFIG_DIRECTORY!, 'config.yaml')
|
||||
const legacyConfigPath = path.join(process.env.TABBY_CONFIG_DIRECTORY!, '../terminus', 'config.yaml')
|
||||
|
||||
|
||||
export function migrateConfig (): void {
|
||||
const configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||
const legacyConfigPath = path.join(app.getPath('userData'), '../terminus', 'config.yaml')
|
||||
if (fs.existsSync(legacyConfigPath) && (
|
||||
!fs.existsSync(configPath) ||
|
||||
fs.statSync(configPath).mtime < fs.statSync(legacyConfigPath).mtime
|
||||
@@ -19,7 +20,6 @@ export function migrateConfig (): void {
|
||||
export function loadConfig (): any {
|
||||
migrateConfig()
|
||||
|
||||
const configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||
if (fs.existsSync(configPath)) {
|
||||
return yaml.load(fs.readFileSync(configPath, 'utf8'))
|
||||
} else {
|
||||
@@ -27,8 +27,6 @@ export function loadConfig (): any {
|
||||
}
|
||||
}
|
||||
|
||||
const configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||
|
||||
export async function saveConfig (content: string): Promise<void> {
|
||||
await writeFile(configPath, content, { encoding: 'utf8' })
|
||||
await writeFile(configPath + '.backup', content, { encoding: 'utf8' })
|
||||
|
@@ -1,17 +1,23 @@
|
||||
import 'v8-compile-cache'
|
||||
import { app, ipcMain, Menu, dialog } from 'electron'
|
||||
|
||||
// set userData Path on portable version
|
||||
import './portable'
|
||||
|
||||
// set defaults of environment variables
|
||||
import 'dotenv/config'
|
||||
process.env.TABBY_PLUGINS ??= ''
|
||||
process.env.TABBY_CONFIG_DIRECTORY ??= app.getPath('userData')
|
||||
|
||||
|
||||
import 'v8-compile-cache'
|
||||
import 'source-map-support/register'
|
||||
import './sentry'
|
||||
import './lru'
|
||||
import { app, ipcMain, Menu, dialog } from 'electron'
|
||||
import { parseArgs } from './cli'
|
||||
import { Application } from './app'
|
||||
import electronDebug = require('electron-debug')
|
||||
import { loadConfig } from './config'
|
||||
|
||||
if (!process.env.TABBY_PLUGINS) {
|
||||
process.env.TABBY_PLUGINS = ''
|
||||
}
|
||||
|
||||
const argv = parseArgs(process.argv, process.cwd())
|
||||
|
||||
|
@@ -26,7 +26,7 @@ abstract class GlasstronWindow extends BrowserWindow {
|
||||
abstract setBlur (_: boolean)
|
||||
}
|
||||
|
||||
const macOSVibrancyType = process.platform === 'darwin' ? compareVersions(macOSRelease().version || '0.0', '10.14', '>=') ? 'under-window' : 'dark' : null
|
||||
const macOSVibrancyType: any = process.platform === 'darwin' ? compareVersions(macOSRelease().version || '0.0', '10.14', '>=') ? 'under-window' : 'dark' : null
|
||||
|
||||
const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`)
|
||||
|
||||
@@ -392,14 +392,15 @@ export class Window {
|
||||
return
|
||||
}
|
||||
|
||||
const symbolColor: string = theme.foreground
|
||||
|
||||
this.window.setTitleBarOverlay(
|
||||
{
|
||||
symbolColor: symbolColor,
|
||||
height: 32,
|
||||
},
|
||||
)
|
||||
if (process.platform === 'win32') {
|
||||
const symbolColor: string = theme.foreground
|
||||
this.window.setTitleBarOverlay(
|
||||
{
|
||||
symbolColor: symbolColor,
|
||||
height: 32,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-title', (event, title) => {
|
||||
|
@@ -38,7 +38,7 @@
|
||||
"@tabby-gang/windows-blurbehind": "^3.0.0",
|
||||
"macos-native-processlist": "^2.1.0",
|
||||
"patch-package": "^6.5.0",
|
||||
"serialport": "11.0.0",
|
||||
"serialport": "11.0.1",
|
||||
"serialport-binding-webserialapi": "^1.0.3",
|
||||
"windows-native-registry": "^3.2.1",
|
||||
"windows-process-tree": "^0.4.0"
|
||||
@@ -47,7 +47,7 @@
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@types/mz": "2.7.4",
|
||||
"@types/node": "20.3.1",
|
||||
"atomically": "^1.7.0",
|
||||
"atomically": "^2.0.2",
|
||||
"filesize": "^9",
|
||||
"ngx-filesize": "^3.0.2"
|
||||
},
|
||||
@@ -64,6 +64,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"*/node-abi": "^2.20.0",
|
||||
"node-gyp": "^10.0.0",
|
||||
"nan": "github:jkleinsc/nan#remove_accessor_signature"
|
||||
}
|
||||
}
|
||||
|
705
app/yarn.lock
705
app/yarn.lock
File diff suppressed because it is too large
Load Diff
570
locale/af-ZA.po
570
locale/af-ZA.po
File diff suppressed because it is too large
Load Diff
568
locale/app.pot
568
locale/app.pot
File diff suppressed because it is too large
Load Diff
570
locale/bg-BG.po
570
locale/bg-BG.po
File diff suppressed because it is too large
Load Diff
570
locale/cs-CZ.po
570
locale/cs-CZ.po
File diff suppressed because it is too large
Load Diff
570
locale/da-DK.po
570
locale/da-DK.po
File diff suppressed because it is too large
Load Diff
570
locale/de-DE.po
570
locale/de-DE.po
File diff suppressed because it is too large
Load Diff
570
locale/en-GB.po
570
locale/en-GB.po
File diff suppressed because it is too large
Load Diff
570
locale/es-ES.po
570
locale/es-ES.po
File diff suppressed because it is too large
Load Diff
570
locale/fr-FR.po
570
locale/fr-FR.po
File diff suppressed because it is too large
Load Diff
570
locale/hr-HR.po
570
locale/hr-HR.po
File diff suppressed because it is too large
Load Diff
592
locale/id-ID.po
592
locale/id-ID.po
File diff suppressed because it is too large
Load Diff
572
locale/it-IT.po
572
locale/it-IT.po
File diff suppressed because it is too large
Load Diff
604
locale/ja-JP.po
604
locale/ja-JP.po
File diff suppressed because it is too large
Load Diff
588
locale/ko-KR.po
588
locale/ko-KR.po
File diff suppressed because it is too large
Load Diff
612
locale/pl-PL.po
612
locale/pl-PL.po
File diff suppressed because it is too large
Load Diff
570
locale/pt-BR.po
570
locale/pt-BR.po
File diff suppressed because it is too large
Load Diff
570
locale/pt-PT.po
570
locale/pt-PT.po
File diff suppressed because it is too large
Load Diff
570
locale/ru-RU.po
570
locale/ru-RU.po
File diff suppressed because it is too large
Load Diff
572
locale/sv-SE.po
572
locale/sv-SE.po
File diff suppressed because it is too large
Load Diff
570
locale/uk-UA.po
570
locale/uk-UA.po
File diff suppressed because it is too large
Load Diff
570
locale/zh-CN.po
570
locale/zh-CN.po
File diff suppressed because it is too large
Load Diff
578
locale/zh-TW.po
578
locale/zh-TW.po
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -26,8 +26,8 @@
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/node": "20.3.1",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"apply-loader": "2.0.0",
|
||||
"axios": "^1.4.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
@@ -39,23 +39,23 @@
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "^6.7.3",
|
||||
"deep-equal": "2.0.5",
|
||||
"electron": "^25.3.0",
|
||||
"electron-builder": "^24.0.0-alpha.1",
|
||||
"electron": "^27.0.4",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-installer-snap": "^5.1.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"gettext-extractor": "^3.5.4",
|
||||
"gettext-extractor": "^3.8.0",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"html-loader": "4.2.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"macos-release": "^3.1.0",
|
||||
"ngx-toastr": "^16.0.2",
|
||||
"node-abi": "^3.45.0",
|
||||
"node-abi": "^3.51.0",
|
||||
"npmlog": "6.0.2",
|
||||
"npx": "^10.2.2",
|
||||
"patch-package": "^6.4.7",
|
||||
@@ -76,7 +76,7 @@
|
||||
"source-code-pro": "^2.38.0",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"source-sans-pro": "3.6.0",
|
||||
"ssh2": "Eugeny/ssh2#9de907d62907d6d45debdcc0ed8dda5b7b19dc7c",
|
||||
"ssh2": "^1.14.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"thenby": "^1.3.4",
|
||||
@@ -95,8 +95,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"*/pug": "^3",
|
||||
"lzma-native": "^8.0.0",
|
||||
"*/node-abi": "^3.33.0",
|
||||
"lzma-native": "^8.0.6",
|
||||
"**/graceful-fs": "^4.2.4",
|
||||
"nan": "2.17.0"
|
||||
},
|
||||
@@ -115,5 +114,8 @@
|
||||
"i18n:push": "crowdin push"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1"
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ builder({
|
||||
armv7l: process.env.ARCH === 'armv7l',
|
||||
arm64: process.env.ARCH === 'arm64',
|
||||
config: {
|
||||
npmRebuild: false,
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
},
|
||||
|
@@ -18,7 +18,7 @@ process.env.APPLE_APP_SPECIFIC_PASSWORD ??= process.env.APPSTORE_PASSWORD
|
||||
|
||||
builder({
|
||||
dir: true,
|
||||
mac: ['pkg', 'zip'],
|
||||
mac: ['dmg', 'zip'],
|
||||
x64: process.env.ARCH === 'x86_64',
|
||||
arm64: process.env.ARCH === 'arm64',
|
||||
config: {
|
||||
|
@@ -16,7 +16,7 @@ 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 { ProfileProvider, ConnectableProfileProvider, QuickConnectProfileProvider, Profile, ConnectableProfile, PartialProfile, ProfileSettingsComponent, ProfileGroup, PartialProfileGroup } from './profileProvider'
|
||||
export { PromptModalComponent } from '../components/promptModal.component'
|
||||
export * from './commands'
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export interface MenuItemOptions {
|
||||
type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio')
|
||||
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'
|
||||
label?: string
|
||||
sublabel?: string
|
||||
enabled?: boolean
|
||||
|
@@ -21,6 +21,10 @@ export interface Profile {
|
||||
isTemplate: boolean
|
||||
}
|
||||
|
||||
export interface ConnectableProfile extends Profile {
|
||||
clearServiceMessagesOnConnect: boolean
|
||||
}
|
||||
|
||||
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||
[K in keyof T]?: T[K]
|
||||
}, 'options'>, 'type'>, 'name'> & {
|
||||
@@ -31,6 +35,21 @@ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProfileGroup {
|
||||
id: string
|
||||
name: string
|
||||
profiles: PartialProfile<Profile>[]
|
||||
defaults: any
|
||||
editable: boolean
|
||||
}
|
||||
|
||||
export type PartialProfileGroup<T extends ProfileGroup> = Omit<Omit<{
|
||||
[K in keyof T]?: T[K]
|
||||
}, 'id'>, 'name'> & {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ProfileSettingsComponent<P extends Profile> {
|
||||
profile: P
|
||||
save?: () => void
|
||||
@@ -39,7 +58,6 @@ export interface ProfileSettingsComponent<P extends Profile> {
|
||||
export abstract class ProfileProvider<P extends Profile> {
|
||||
id: string
|
||||
name: string
|
||||
supportsQuickConnect = false
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||
configDefaults = {}
|
||||
|
||||
@@ -53,13 +71,15 @@ export abstract class ProfileProvider<P extends Profile> {
|
||||
|
||||
abstract getDescription (profile: PartialProfile<P>): string
|
||||
|
||||
quickConnect (query: string): PartialProfile<P>|null {
|
||||
return null
|
||||
}
|
||||
|
||||
intoQuickConnectString (profile: P): string|null {
|
||||
return null
|
||||
}
|
||||
|
||||
deleteProfile (profile: P): void { }
|
||||
}
|
||||
|
||||
export abstract class ConnectableProfileProvider<P extends ConnectableProfile> extends ProfileProvider<P> {}
|
||||
|
||||
export abstract class QuickConnectProfileProvider<P extends ConnectableProfile> extends ConnectableProfileProvider<P> {
|
||||
|
||||
abstract quickConnect (query: string): PartialProfile<P>|null
|
||||
|
||||
abstract intoQuickConnectString (profile: P): string|null
|
||||
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from './api/hostApp'
|
||||
import { CLIHandler, CLIEvent } from './api/cli'
|
||||
import { HostWindowService } from './api/hostWindow'
|
||||
import { QuickConnectProfileProvider } from './api/profileProvider'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
|
||||
@Injectable()
|
||||
@@ -27,6 +28,10 @@ export class ProfileCLIHandler extends CLIHandler {
|
||||
this.handleOpenRecentProfile(event.argv.profileNumber)
|
||||
return true
|
||||
}
|
||||
if (op === 'quickConnect') {
|
||||
this.handleOpenQuickConnect(event.argv.providerId, event.argv.query)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -48,6 +53,21 @@ export class ProfileCLIHandler extends CLIHandler {
|
||||
this.profiles.openNewTabForProfile(profiles[profileNumber])
|
||||
this.hostWindow.bringToFront()
|
||||
}
|
||||
|
||||
private async handleOpenQuickConnect (providerId: string, query: string) {
|
||||
const provider = this.profiles.getProviders().find(x => x.id === providerId)
|
||||
if(!provider || !(provider instanceof QuickConnectProfileProvider)) {
|
||||
console.error(`Requested provider "${providerId}" not found`)
|
||||
return
|
||||
}
|
||||
const profile = provider.quickConnect(query)
|
||||
if(!profile) {
|
||||
console.error(`Could not parse quick connect query "${query}"`)
|
||||
return
|
||||
}
|
||||
this.profiles.openNewTabForProfile(profile)
|
||||
this.hostWindow.bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@@ -18,7 +18,7 @@ export class CoreCommandProvider extends CommandProvider {
|
||||
}
|
||||
|
||||
async activate () {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||
if (profile) {
|
||||
this.profilesService.launchProfile(profile)
|
||||
}
|
||||
|
@@ -1,21 +1,21 @@
|
||||
title-bar(
|
||||
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
(dblclick)='hostWindow.toggleMaximize()',
|
||||
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.dock == "off" && isTitleBarNeeded()',
|
||||
(dblclick)='toggleMaximize()',
|
||||
[hideControls]='hostApp.platform !== Platform.Linux && !hostWindow.isFullscreen',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullscreen'
|
||||
)
|
||||
|
||||
.content(
|
||||
*ngIf='ready',
|
||||
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left"',
|
||||
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left" || config.store.appearance.tabsLocation == "right"',
|
||||
[class.tabs-on-left]='hasVerticalTabs() && config.store.appearance.tabsLocation == "left"',
|
||||
[class.tabs-titlebar-enabled]='config.store.appearance.frame == "full"',
|
||||
[class.tabs-titlebar-enabled]='isTitleBarNeeded()',
|
||||
[class.tabs-on-right]='hasVerticalTabs() && config.store.appearance.tabsLocation == "right"',
|
||||
)
|
||||
.tab-bar(
|
||||
*ngIf='!hostWindow.isFullscreen || config.store.appearance.tabsInFullscreen',
|
||||
[class.tab-bar-no-controls-overlay]='hostApp.platform == Platform.macOS',
|
||||
(dblclick)='hostWindow.toggleMaximize()'
|
||||
(dblclick)='!isTitleBarNeeded() && toggleMaximize()'
|
||||
)
|
||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
|
||||
&& !hostWindow.isFullscreen \
|
||||
@@ -35,8 +35,7 @@ title-bar(
|
||||
[@animateTab]='{value: "in", params: {size: targetTabSize}}',
|
||||
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
|
||||
(click)='app.selectTab(tab)',
|
||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
|
||||
[class.fully-draggable]='hostApp.platform !== Platform.macOS'
|
||||
)
|
||||
|
||||
.btn-group.background
|
||||
@@ -65,7 +64,11 @@ title-bar(
|
||||
(transfersChange)='onTransfersChange()'
|
||||
)
|
||||
|
||||
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
.btn-space.background(
|
||||
[class.persistent]='config.store.appearance.frame == "thin"',
|
||||
[class.drag]='config.store.appearance.frame == "thin" \
|
||||
&& ((config.store.appearance.tabsLocation !== "left" && config.store.appearance.tabsLocation !== "right") || hostApp.platform !== Platform.macOS)'
|
||||
)
|
||||
|
||||
.btn-group.background
|
||||
.d-flex(
|
||||
@@ -85,7 +88,9 @@ title-bar(
|
||||
|
||||
window-controls.background(
|
||||
*ngIf='config.store.appearance.frame == "thin" \
|
||||
&& (hostApp.platform == Platform.Linux)',
|
||||
&& config.store.appearance.tabsLocation !== "left" \
|
||||
&& config.store.appearance.tabsLocation !== "right" \
|
||||
&& hostApp.platform == Platform.Linux',
|
||||
)
|
||||
|
||||
div.window-controls-spacer(
|
||||
|
@@ -62,7 +62,7 @@ $tab-border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-space {
|
||||
.btn-space {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
@@ -126,15 +126,18 @@ $tab-border-radius: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&>.drag-space {
|
||||
&>.btn-space {
|
||||
min-width: 1px;
|
||||
flex: 1 0 1%;
|
||||
-webkit-app-region: drag;
|
||||
|
||||
.tabs-on-top & {
|
||||
margin-top: 2px; // for window resizing
|
||||
}
|
||||
|
||||
&.drag {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
&.persistent {
|
||||
// min-width: 72px; // 2 x 36 px height, ie 2 squares
|
||||
// Given WCO on Windows, the min-width of the window buttons is about 138px.
|
||||
|
@@ -240,4 +240,19 @@ export class AppRootComponent {
|
||||
return (await this.commands.getCommands({ tab: this.app.activeTab ?? undefined }))
|
||||
.filter(x => x.locations?.includes(aboveZero ? CommandLocation.RightToolbar : CommandLocation.LeftToolbar))
|
||||
}
|
||||
|
||||
toggleMaximize (): void {
|
||||
this.hostWindow.toggleMaximize()
|
||||
}
|
||||
|
||||
protected isTitleBarNeeded (): boolean {
|
||||
return (
|
||||
this.config.store.appearance.frame === 'full'
|
||||
||
|
||||
this.hostApp.platform !== Platform.macOS
|
||||
&& this.config.store.appearance.frame === 'thin'
|
||||
&& this.config.store.appearance.tabsLocation !== 'top'
|
||||
&& this.config.store.appearance.tabsLocation !== 'bottom'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -75,6 +75,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
private titleChange = new Subject<string>()
|
||||
private focused = new Subject<void>()
|
||||
private blurred = new Subject<void>()
|
||||
private visibility = new Subject<boolean>()
|
||||
private progress = new Subject<number|null>()
|
||||
private activity = new Subject<boolean>()
|
||||
private destroyed = new Subject<void>()
|
||||
@@ -83,6 +84,8 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
|
||||
get focused$ (): Observable<void> { return this.focused }
|
||||
get blurred$ (): Observable<void> { return this.blurred }
|
||||
/* @hidden */
|
||||
get visibility$ (): Observable<boolean> { return this.visibility }
|
||||
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 }
|
||||
@@ -177,6 +180,11 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||
this.blurred.next()
|
||||
}
|
||||
|
||||
/* @hidden */
|
||||
emitVisibility (visibility: boolean): void {
|
||||
this.visibility.next(visibility)
|
||||
}
|
||||
|
||||
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
|
||||
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
|
||||
this.viewContainer = container
|
||||
|
@@ -18,45 +18,58 @@ export class SelectorModalComponent<T> {
|
||||
@Input() selectedIndex = 0
|
||||
hasGroups = false
|
||||
@ViewChildren('item') itemChildren: QueryList<ElementRef>
|
||||
private preventEdit: boolean
|
||||
|
||||
constructor (
|
||||
public modalInstance: NgbActiveModal,
|
||||
) { }
|
||||
constructor (public modalInstance: NgbActiveModal) {
|
||||
this.preventEdit = false
|
||||
}
|
||||
|
||||
ngOnInit (): void {
|
||||
this.onFilterChange()
|
||||
this.hasGroups = this.options.some(x => x.group)
|
||||
}
|
||||
|
||||
@HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void {
|
||||
if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) {
|
||||
this.selectedIndex -= 10
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) {
|
||||
this.selectedIndex += 10
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
this.selectedIndex--
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
this.selectedIndex++
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'Enter') {
|
||||
this.selectOption(this.filteredOptions[this.selectedIndex])
|
||||
} else if (event.key === 'Escape') {
|
||||
@HostListener('keydown', ['$event']) onKeyDown (event: KeyboardEvent): void {
|
||||
if (event.key === 'Escape') {
|
||||
this.close()
|
||||
}
|
||||
if (event.key === 'Backspace' && this.canEditSelected()) {
|
||||
event.preventDefault()
|
||||
this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
|
||||
this.onFilterChange()
|
||||
}
|
||||
} else if (this.filteredOptions.length > 0) {
|
||||
if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) {
|
||||
this.selectedIndex -= Math.min(10, Math.max(1, this.selectedIndex))
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) {
|
||||
this.selectedIndex += Math.min(10, Math.max(1, this.filteredOptions.length - this.selectedIndex - 1))
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
this.selectedIndex--
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
this.selectedIndex++
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'Enter') {
|
||||
this.selectOption(this.filteredOptions[this.selectedIndex])
|
||||
} else if (event.key === 'Backspace' && !this.preventEdit) {
|
||||
if (this.canEditSelected()) {
|
||||
event.preventDefault()
|
||||
this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
|
||||
this.onFilterChange()
|
||||
} else {
|
||||
this.preventEdit = true
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
|
||||
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
})
|
||||
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
|
||||
|
||||
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void {
|
||||
if (event.key === 'Backspace' && this.preventEdit) {
|
||||
this.preventEdit = false
|
||||
}
|
||||
}
|
||||
|
||||
onFilterChange (): void {
|
||||
@@ -76,7 +89,7 @@ export class SelectorModalComponent<T> {
|
||||
{ sort: true },
|
||||
).search(f)
|
||||
|
||||
this.options.filter(x => x.freeInputPattern).forEach(freeOption => {
|
||||
this.options.filter(x => x.freeInputPattern).sort(firstBy<SelectorOption<T>, number>(x => x.weight ?? 0)).forEach(freeOption => {
|
||||
if (!this.filteredOptions.includes(freeOption)) {
|
||||
this.filteredOptions.push(freeOption)
|
||||
}
|
||||
|
@@ -275,6 +275,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
}
|
||||
})
|
||||
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
|
||||
this.visibility$.subscribe(visibility => this.getAllTabs().forEach(x => x.emitVisibility(visibility)))
|
||||
|
||||
this.tabAdded$.subscribe(() => this.updateTitle())
|
||||
this.tabRemoved$.subscribe(() => this.updateTitle())
|
||||
|
@@ -1,10 +1,10 @@
|
||||
.container.mt-5.mb-5
|
||||
.mb-4
|
||||
.container.mt-3.mb-3
|
||||
.mb-3
|
||||
.tabby-logo
|
||||
h1.tabby-title Tabby
|
||||
sup α
|
||||
|
||||
.text-center.mb-5(translate) Thank you for downloading Tabby!
|
||||
.text-center.mb-3(translate) Thank you for downloading Tabby!
|
||||
|
||||
.form-line
|
||||
.header
|
||||
@@ -16,13 +16,54 @@
|
||||
*ngFor='let lang of allLanguages'
|
||||
) {{lang.name}}
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(translate) Switch color scheme
|
||||
|
||||
.btn-group(role='group')
|
||||
input.btn-check(
|
||||
type='radio',
|
||||
name='colorSchemeMode',
|
||||
[(ngModel)]='config.store.appearance.colorSchemeMode',
|
||||
(ngModelChange)='config.save()',
|
||||
id='colorSchemeModeAuto',
|
||||
[value]='"auto"'
|
||||
)
|
||||
label.btn.btn-secondary(
|
||||
for='colorSchemeModeAuto'
|
||||
)
|
||||
span(translate) From system
|
||||
input.btn-check(
|
||||
type='radio',
|
||||
name='colorSchemeMode',
|
||||
[(ngModel)]='config.store.appearance.colorSchemeMode',
|
||||
(ngModelChange)='config.save()',
|
||||
id='colorSchemeModeDark',
|
||||
[value]='"dark"'
|
||||
)
|
||||
label.btn.btn-secondary(
|
||||
for='colorSchemeModeDark'
|
||||
)
|
||||
span(translate) Always dark
|
||||
input.btn-check(
|
||||
type='radio',
|
||||
name='colorSchemeMode',
|
||||
[(ngModel)]='config.store.appearance.colorSchemeMode',
|
||||
(ngModelChange)='config.save()',
|
||||
id='colorSchemeModeLight',
|
||||
[value]='"light"'
|
||||
)
|
||||
label.btn.btn-secondary(
|
||||
for='colorSchemeModeLight'
|
||||
)
|
||||
span(translate) Always light
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(translate) Enable analytics
|
||||
.description(translate) Help track the number of Tabby installs across the world!
|
||||
toggle([(ngModel)]='config.store.enableAnalytics')
|
||||
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(translate) Enable global hotkey (Ctrl-Space)
|
||||
|
@@ -6,3 +6,8 @@
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tabby-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
@@ -9,5 +9,6 @@ export class CoreConfigProvider extends ConfigProvider {
|
||||
[Platform.Linux]: require('./configDefaults.linux.yaml').default,
|
||||
[Platform.Web]: require('./configDefaults.web.yaml').default,
|
||||
}
|
||||
|
||||
defaults = require('./configDefaults.yaml').default
|
||||
}
|
||||
|
@@ -96,5 +96,3 @@ hotkeys:
|
||||
- '⌘-Shift-E'
|
||||
command-selector:
|
||||
- '⌘-Shift-P'
|
||||
appearance:
|
||||
vibrancy: true
|
||||
|
@@ -19,6 +19,7 @@ appearance:
|
||||
vibrancyType: 'blur'
|
||||
lastTabClosesWindow: false
|
||||
spaciness: 1
|
||||
colorSchemeMode: 'dark'
|
||||
terminal:
|
||||
showBuiltinProfiles: true
|
||||
showRecentProfiles: 3
|
||||
@@ -31,6 +32,7 @@ hotkeys:
|
||||
profile-selectors:
|
||||
__nonStructural: true
|
||||
profiles: []
|
||||
groups: []
|
||||
profileDefaults:
|
||||
__nonStructural: true
|
||||
ssh:
|
||||
|
@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
|
||||
import { TranslateService } from '@ngx-translate/core'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { PartialProfile, Profile } from './api'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -268,7 +267,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
return [
|
||||
...this.hotkeys,
|
||||
...profiles.map(profile => ({
|
||||
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
||||
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`,
|
||||
name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
|
||||
})),
|
||||
...this.profilesService.getProviders().map(provider => ({
|
||||
@@ -278,7 +277,4 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
]
|
||||
}
|
||||
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return (profile.id ?? profile.name).replace(/\./g, '-')
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ 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, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, QuickConnectProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
@@ -177,7 +177,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
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)
|
||||
const profile = profiles.find(x => ProfilesService.getProfileHotkeyName(x) === id)
|
||||
if (profile) {
|
||||
profilesService.openNewTabForProfile(profile)
|
||||
}
|
||||
@@ -188,10 +188,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
this.showSelector(provider)
|
||||
this.showSelector(provider).catch(() => null)
|
||||
}
|
||||
if (hotkey === 'command-selector') {
|
||||
commands.showSelector()
|
||||
commands.showSelector().catch(() => null)
|
||||
}
|
||||
|
||||
if (hotkey === 'profile-selector') {
|
||||
@@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
callback: () => this.profilesService.openNewTabForProfile(p),
|
||||
}))
|
||||
|
||||
if (provider.supportsQuickConnect) {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
|
@@ -230,11 +230,13 @@ export class AppService {
|
||||
if (this._activeTab) {
|
||||
this._activeTab.clearActivity()
|
||||
this._activeTab.emitBlurred()
|
||||
this._activeTab.emitVisibility(false)
|
||||
}
|
||||
this._activeTab = tab
|
||||
this.activeTabChange.next(tab)
|
||||
setImmediate(() => {
|
||||
this._activeTab?.emitFocused()
|
||||
this._activeTab?.emitVisibility(true)
|
||||
})
|
||||
this.hostWindow.setTitle(this._activeTab?.title)
|
||||
}
|
||||
|
@@ -101,7 +101,7 @@ export class CommandService {
|
||||
context.tab = tab.getFocusedTab() ?? undefined
|
||||
}
|
||||
const commands = await this.getCommands(context)
|
||||
await this.selector.show(
|
||||
return this.selector.show(
|
||||
this.translate.instant('Commands'),
|
||||
commands.map(c => ({
|
||||
name: c.label,
|
||||
|
@@ -10,6 +10,7 @@ import { PlatformService } from '../api/platform'
|
||||
import { HostAppService } from '../api/hostApp'
|
||||
import { Vault, VaultService } from './vault.service'
|
||||
import { serializeFunction } from '../utils'
|
||||
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
|
||||
const deepmerge = require('deepmerge')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@@ -364,6 +365,55 @@ export class ConfigService {
|
||||
}
|
||||
config.version = 4
|
||||
}
|
||||
if (config.version < 5) {
|
||||
const groups: PartialProfileGroup<ProfileGroup>[] = []
|
||||
for (const p of config.profiles ?? []) {
|
||||
if (!(p.group ?? '').trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
let group = groups.find(x => x.name === p.group)
|
||||
if (!group) {
|
||||
group = {
|
||||
id: `${uuidv4()}`,
|
||||
name: `${p.group}`,
|
||||
}
|
||||
groups.push(group)
|
||||
}
|
||||
p.group = group.id
|
||||
}
|
||||
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
for (const g of groups) {
|
||||
if (profileGroupCollapsed[g.name]) {
|
||||
const collapsed = profileGroupCollapsed[g.name]
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileGroupCollapsed[g.name]
|
||||
profileGroupCollapsed[g.id] = collapsed
|
||||
}
|
||||
}
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
|
||||
config.groups = groups
|
||||
config.version = 5
|
||||
}
|
||||
if (config.version < 6) {
|
||||
if (config.ssh?.clearServiceMessagesOnConnect === false) {
|
||||
config.profileDefaults ??= {}
|
||||
config.profileDefaults.ssh ??= {}
|
||||
config.profileDefaults.ssh.clearServiceMessagesOnConnect = false
|
||||
delete config.ssh?.clearServiceMessagesOnConnect
|
||||
}
|
||||
config.version = 6
|
||||
}
|
||||
if (config.version < 7) {
|
||||
if (!config.configSync?.host || config.configSync?.host === 'https://api.tabby.sh') {
|
||||
config.configSync ??= {}
|
||||
delete config.configSync.host
|
||||
delete config.configSync.token
|
||||
}
|
||||
config.version = 7
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeDecryptConfig (store) {
|
||||
|
@@ -13,8 +13,9 @@ export class FileProvidersService {
|
||||
) { }
|
||||
|
||||
async selectAndStoreFile (description: string): Promise<string> {
|
||||
const p = await this.selectProvider()
|
||||
return p.selectAndStoreFile(description)
|
||||
return this.selectProvider().then(p => {
|
||||
return p.selectAndStoreFile(description)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveFile (key: string): Promise<Buffer> {
|
||||
|
@@ -2,12 +2,15 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import { TranslateService } from '@ngx-translate/core'
|
||||
import { NewTabParameters } from './tabs.service'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
|
||||
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, 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'
|
||||
import deepClone from 'clone-deep'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import slugify from 'slugify'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProfilesService {
|
||||
@@ -36,6 +39,127 @@ export class ProfilesService {
|
||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
||||
) { }
|
||||
|
||||
/*
|
||||
* Methods used to interract with ProfileProvider
|
||||
*/
|
||||
|
||||
getProviders (): ProfileProvider<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
* Methods used to interract with Profile
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return ConfigProxy for a given Profile
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||
*/
|
||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): T {
|
||||
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Array of Profiles
|
||||
* arg: includeBuiltin (default: true) -> include BuiltinProfiles
|
||||
* arg: clone (default: false) -> return deepclone Array
|
||||
*/
|
||||
async getProfiles (options?: { includeBuiltin?: boolean, clone?: boolean }): Promise<PartialProfile<Profile>[]> {
|
||||
let list = this.config.store.profiles ?? []
|
||||
if (options?.includeBuiltin ?? true) {
|
||||
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
||||
list = [
|
||||
...this.config.store.profiles ?? [],
|
||||
...lists.reduce((a, b) => a.concat(b), []),
|
||||
]
|
||||
}
|
||||
|
||||
const sortKey = p => `${this.resolveProfileGroupName(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 options?.clone ? deepClone(list) : list
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new Profile in config
|
||||
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||
*/
|
||||
async newProfile (profile: PartialProfile<Profile>, options?: { genId?: boolean }): Promise<void> {
|
||||
if (options?.genId ?? true) {
|
||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||
}
|
||||
|
||||
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||
if (cProfile) {
|
||||
throw new Error(`Cannot insert new Profile, duplicated Id: ${profile.id}`)
|
||||
}
|
||||
|
||||
this.config.store.profiles.push(profile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a Profile in config
|
||||
*/
|
||||
async writeProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||
if (cProfile) {
|
||||
// Fully replace the config
|
||||
for (const k in cProfile) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete cProfile[k]
|
||||
}
|
||||
Object.assign(cProfile, profile)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Profile from config
|
||||
*/
|
||||
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||
this.config.store.profiles = this.config.store.profiles.filter(p => p.id !== profile.id)
|
||||
|
||||
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all Profiles from config using option filter
|
||||
* arg: filter (p: PartialProfile<Profile>) => boolean -> predicate used to decide which profiles have to be deleted
|
||||
*/
|
||||
async bulkDeleteProfiles (filter: (p: PartialProfile<Profile>) => boolean): Promise<void> {
|
||||
for (const profile of this.config.store.profiles.filter(filter)) {
|
||||
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||
|
||||
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
}
|
||||
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => !filter(x))
|
||||
}
|
||||
|
||||
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
|
||||
const params = await this.newTabParametersForProfile(profile)
|
||||
if (params) {
|
||||
@@ -63,52 +187,40 @@ export class ProfilesService {
|
||||
return params
|
||||
}
|
||||
|
||||
getProviders (): ProfileProvider<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
await this.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)
|
||||
}
|
||||
|
||||
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
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return (profile.id ?? profile.name).replace(/\./g, '-')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
/*
|
||||
* Methods used to interract with Profile Selector
|
||||
*/
|
||||
|
||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||
const provider = this.providerForProfile(fullProfile)
|
||||
const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
|
||||
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
|
||||
return {
|
||||
...profile,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
group: profile.group || '',
|
||||
group: this.resolveProfileGroupName(profile.group ?? ''),
|
||||
freeInputEquivalent,
|
||||
description: provider?.getDescription(fullProfile),
|
||||
}
|
||||
}
|
||||
|
||||
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
return recentProfiles
|
||||
}
|
||||
|
||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||
if (this.selector.active) {
|
||||
return Promise.resolve(null)
|
||||
@@ -118,12 +230,12 @@ export class ProfilesService {
|
||||
try {
|
||||
const recentProfiles = this.getRecentProfiles()
|
||||
|
||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
group: this.translate.instant('Recent'),
|
||||
icon: 'fas fa-history',
|
||||
color: p.color,
|
||||
weight: -2,
|
||||
weight: i - (recentProfiles.length + 1),
|
||||
callback: async () => {
|
||||
if (p.id) {
|
||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||
@@ -177,30 +289,38 @@ export class ProfilesService {
|
||||
})
|
||||
} catch { }
|
||||
|
||||
this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
description: `(${provider.name.toUpperCase()})`,
|
||||
icon: 'fas fa-arrow-right',
|
||||
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
||||
callback: query => {
|
||||
const profile = provider.quickConnect(query)
|
||||
resolve(profile)
|
||||
},
|
||||
})
|
||||
this.getProviders().forEach(provider => {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
description: `(${provider.name.toUpperCase()})`,
|
||||
icon: 'fas fa-arrow-right',
|
||||
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
||||
callback: query => {
|
||||
const profile = provider.quickConnect(query)
|
||||
resolve(profile)
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
|
||||
await this.selector.show(this.translate.instant('Select profile or enter an address'), options).catch(() => reject())
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
return recentProfiles
|
||||
}
|
||||
|
||||
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
|
||||
for (const provider of this.getProviders()) {
|
||||
if (provider.supportsQuickConnect) {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
const profile = provider.quickConnect(query)
|
||||
if (profile) {
|
||||
return profile
|
||||
@@ -211,27 +331,178 @@ export class ProfilesService {
|
||||
return null
|
||||
}
|
||||
|
||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
|
||||
/*
|
||||
* Methods used to interract with Profile/ProfileGroup/Global defaults
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return global defaults for a given profile provider
|
||||
* Always return something, empty object if no defaults found
|
||||
*/
|
||||
getProviderDefaults (provider: ProfileProvider<Profile>): any {
|
||||
const defaults = this.config.store.profileDefaults
|
||||
return defaults[provider.id] ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set global defaults for a given profile provider
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
setProviderDefaults (provider: ProfileProvider<Profile>, pdefaults: any): void {
|
||||
this.config.store.profileDefaults[provider.id] = pdefaults
|
||||
}
|
||||
|
||||
/**
|
||||
* Return defaults for a given profile
|
||||
* Always return something, empty object if no defaults found
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||
*/
|
||||
getProfileDefaults (profile: PartialProfile<Profile>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): any[] {
|
||||
const provider = this.providerForProfile(profile)
|
||||
const defaults = [
|
||||
|
||||
return [
|
||||
this.profileDefaults,
|
||||
provider?.configDefaults ?? {},
|
||||
!provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
|
||||
].reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {},
|
||||
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {},
|
||||
]
|
||||
}
|
||||
|
||||
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
await this.openNewTabForProfile(profile)
|
||||
/*
|
||||
* Methods used to interract with ProfileGroup
|
||||
*/
|
||||
|
||||
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 = []
|
||||
/**
|
||||
* Synchronously return an Array of the existing ProfileGroups
|
||||
* Does not return builtin groups
|
||||
*/
|
||||
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] {
|
||||
return deepClone(this.config.store.groups ?? [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Array of the existing ProfileGroups
|
||||
* arg: includeProfiles (default: false) -> if false, does not fill up the profiles field of ProfileGroup
|
||||
* arg: includeNonUserGroup (default: false) -> if false, does not add built-in and ungrouped groups
|
||||
*/
|
||||
async getProfileGroups (options?: { includeProfiles?: boolean, includeNonUserGroup?: boolean }): Promise<PartialProfileGroup<ProfileGroup>[]> {
|
||||
let profiles: PartialProfile<Profile>[] = []
|
||||
if (options?.includeProfiles) {
|
||||
profiles = await this.getProfiles({ includeBuiltin: options.includeNonUserGroup, clone: true })
|
||||
}
|
||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||
|
||||
let groups: PartialProfileGroup<ProfileGroup>[] = this.getSyncProfileGroups()
|
||||
groups = groups.map(x => {
|
||||
x.editable = true
|
||||
|
||||
if (options?.includeProfiles) {
|
||||
x.profiles = profiles.filter(p => p.group === x.id)
|
||||
profiles = profiles.filter(p => p.group !== x.id)
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
|
||||
if (options?.includeNonUserGroup) {
|
||||
const builtInGroups: PartialProfileGroup<ProfileGroup>[] = []
|
||||
builtInGroups.push({
|
||||
id: 'built-in',
|
||||
name: this.translate.instant('Built-in'),
|
||||
editable: false,
|
||||
profiles: [],
|
||||
})
|
||||
|
||||
const ungrouped: PartialProfileGroup<ProfileGroup> = {
|
||||
id: 'ungrouped',
|
||||
name: this.translate.instant('Ungrouped'),
|
||||
editable: false,
|
||||
}
|
||||
|
||||
if (options.includeProfiles) {
|
||||
for (const profile of profiles.filter(p => p.isBuiltin)) {
|
||||
let group: PartialProfileGroup<ProfileGroup> | undefined = builtInGroups.find(g => g.id === slugify(profile.group ?? 'built-in'))
|
||||
if (!group) {
|
||||
group = {
|
||||
id: `${slugify(profile.group!)}`,
|
||||
name: `${profile.group!}`,
|
||||
editable: false,
|
||||
profiles: [],
|
||||
}
|
||||
builtInGroups.push(group)
|
||||
}
|
||||
|
||||
group.profiles!.push(profile)
|
||||
}
|
||||
|
||||
ungrouped.profiles = profiles.filter(p => !p.isBuiltin)
|
||||
}
|
||||
|
||||
groups = groups.concat(builtInGroups)
|
||||
groups.push(ungrouped)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new ProfileGroup in config
|
||||
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||
*/
|
||||
async newProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { genId?: boolean }): Promise<void> {
|
||||
if (options?.genId ?? true) {
|
||||
group.id = `${uuidv4()}`
|
||||
}
|
||||
|
||||
const cProfileGroup = this.config.store.groups.find(p => p.id === group.id)
|
||||
if (cProfileGroup) {
|
||||
throw new Error(`Cannot insert new ProfileGroup, duplicated Id: ${group.id}`)
|
||||
}
|
||||
|
||||
this.config.store.groups.push(group)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a ProfileGroup in config
|
||||
*/
|
||||
async writeProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||
delete group.profiles
|
||||
delete group.editable
|
||||
|
||||
const cGroup = this.config.store.groups.find(g => g.id === group.id)
|
||||
if (cGroup) {
|
||||
Object.assign(cGroup, group)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a ProfileGroup from config
|
||||
*/
|
||||
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { deleteProfiles?: boolean }): Promise<void> {
|
||||
this.config.store.groups = this.config.store.groups.filter(g => g.id !== group.id)
|
||||
if (options?.deleteProfiles) {
|
||||
await this.bulkDeleteProfiles((p) => p.group === group.id)
|
||||
} else {
|
||||
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) {
|
||||
delete profile.group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return ProfileGroup Name from ProfileGroup ID
|
||||
*/
|
||||
resolveProfileGroupName (groupId: string): string {
|
||||
return this.config.store.groups.find(g => g.id === groupId)?.name ?? groupId
|
||||
}
|
||||
|
||||
/**
|
||||
* Return defaults for a given group ID and provider
|
||||
* Always return something, empty object if no defaults found
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
*/
|
||||
getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider<Profile>): any {
|
||||
return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { Subject, Observable } from 'rxjs'
|
||||
import * as Color from 'color'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { Theme } from '../api/theme'
|
||||
import { PlatformService } from '../api/platform'
|
||||
import { PlatformService, PlatformTheme } from '../api/platform'
|
||||
import { NewTheme } from '../theme'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -194,7 +194,14 @@ export class ThemesService {
|
||||
|
||||
/// @hidden
|
||||
_getActiveColorScheme (): any {
|
||||
if (this.platform.getTheme() === 'light') {
|
||||
let theme: PlatformTheme = 'dark'
|
||||
if (this.config.store.appearance.colorSchemeMode === 'light') {
|
||||
theme = 'light'
|
||||
} else if (this.config.store.appearance.colorSchemeMode === 'auto') {
|
||||
theme = this.platform.getTheme()
|
||||
}
|
||||
|
||||
if (theme === 'light') {
|
||||
return this.config.store.terminal.lightColorScheme
|
||||
} else {
|
||||
return this.config.store.terminal.colorScheme
|
||||
|
@@ -285,7 +285,7 @@ export class VaultFileProvider extends FileProvider {
|
||||
icon: 'fas fa-file',
|
||||
result: f,
|
||||
})),
|
||||
])
|
||||
]).catch(() => null)
|
||||
if (result) {
|
||||
return `${this.prefix}${result.key.id}`
|
||||
}
|
||||
|
@@ -149,7 +149,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
click: async () => {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('Profile name')
|
||||
const name = (await modal.result)?.value
|
||||
const name = (await modal.result.catch(() => null))?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
@@ -262,7 +262,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
|
||||
}
|
||||
|
||||
async switchTabProfile (tab: BaseTabComponent) {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||
if (!profile) {
|
||||
return
|
||||
}
|
||||
|
@@ -22,5 +22,6 @@ export class ElectronConfigProvider extends ConfigProvider {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaults = {}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import { ElectronHostWindow } from './hostWindow.service'
|
||||
import { ShellIntegrationService } from './shellIntegration.service'
|
||||
import { ElectronHostAppService } from './hostApp.service'
|
||||
import { PlatformTheme } from '../../../tabby-core/src/api/platform'
|
||||
import { configPath } from '../../../app/lib/config'
|
||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||
|
||||
/* eslint-disable block-scoped-var */
|
||||
@@ -36,7 +37,7 @@ export class ElectronPlatformService extends PlatformService {
|
||||
private translate: TranslateService,
|
||||
) {
|
||||
super()
|
||||
this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
|
||||
this.configPath = configPath
|
||||
|
||||
electron.ipcRenderer.on('host:display-metrics-changed', () => {
|
||||
this.zone.run(() => this.displayMetricsChanged.next())
|
||||
|
@@ -33,6 +33,7 @@ export class ShellIntegrationService {
|
||||
command: 'paste "%V"',
|
||||
},
|
||||
]
|
||||
|
||||
private constructor (
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
|
@@ -25,7 +25,7 @@ export class ElectronUpdaterService extends UpdaterService {
|
||||
super()
|
||||
this.logger = log.create('updater')
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
if (process.platform === 'linux' || process.env.PORTABLE_EXECUTABLE_FILE) {
|
||||
this.electronUpdaterAvailable = false
|
||||
return
|
||||
}
|
||||
|
@@ -70,6 +70,7 @@ export class PluginManagerService {
|
||||
map(plugins => {
|
||||
const mapping: Record<string, PluginInfo[]> = {}
|
||||
for (const p of plugins) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
mapping[p.name] ??= []
|
||||
mapping[p.name].push(p)
|
||||
}
|
||||
|
@@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
|
||||
import { LogService, NotificationsService } from 'tabby-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Injector, NgZone } from '@angular/core'
|
||||
import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
||||
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
||||
import { SerialService } from './services/serial.service'
|
||||
|
||||
export interface SerialProfile extends BaseTerminalProfile {
|
||||
export interface SerialProfile extends ConnectableTerminalProfile {
|
||||
options: SerialProfileOptions
|
||||
}
|
||||
|
||||
|
@@ -87,6 +87,11 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
.description(translate) Sends data one byte at a time
|
||||
toggle([(ngModel)]='profile.options.slowSend')
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink, translate) Colors
|
||||
ng-template(ngbNavContent)
|
||||
color-scheme-selector([(model)]='profile.terminalColorScheme')
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink, translate) Login scripts
|
||||
ng-template(ngbNavContent)
|
||||
|
@@ -2,14 +2,14 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
||||
import slugify from 'slugify'
|
||||
import deepClone from 'clone-deep'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfileProvider, NewTabParameters, SelectorService, HostAppService, Platform, TranslateService } from 'tabby-core'
|
||||
import { NewTabParameters, SelectorService, HostAppService, Platform, TranslateService, ConnectableProfileProvider } from 'tabby-core'
|
||||
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
|
||||
import { SerialTabComponent } from './components/serialTab.component'
|
||||
import { SerialService } from './services/serial.service'
|
||||
import { BAUD_RATES, SerialProfile } from './api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
||||
export class SerialProfilesService extends ConnectableProfileProvider<SerialProfile> {
|
||||
id = 'serial'
|
||||
name = _('Serial')
|
||||
settingsComponent = SerialProfileSettingsComponent
|
||||
@@ -32,6 +32,7 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
||||
slowSend: false,
|
||||
input: { backspace: 'backspace' },
|
||||
},
|
||||
clearServiceMessagesOnConnect: false,
|
||||
}
|
||||
|
||||
constructor (
|
||||
|
@@ -18,8 +18,8 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/marked": "^4.0.8",
|
||||
"marked": "^4.2.12",
|
||||
"@types/marked": "^5.0.1",
|
||||
"marked": "^5.1.2",
|
||||
"ngx-infinite-scroll": "^16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -20,7 +20,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
)
|
||||
i.fas.fa-external-link-alt
|
||||
|
||||
.form-line
|
||||
.form-line(*ngIf='config.store.configSync.host')
|
||||
.header
|
||||
.title(translate) Secret sync token
|
||||
.description(translate) Get it from the Tabby Web settings window
|
||||
@@ -36,6 +36,11 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
i.fas.fa-fw.fa-check.text-success(*ngIf='connectionSuccessful')
|
||||
i.fas.fa-fw.fa-exclamation-triangle.text-danger(*ngIf='connectionSuccessful === false')
|
||||
|
||||
.alert.alert-info.d-flex.align-items-center
|
||||
.me-auto
|
||||
span(translate) Config sync requires an instance of the Tabby Web service.
|
||||
a.ml-1((click)='openTabbyWebInfo()', href='#', translate) Learn more
|
||||
|
||||
ng-container(*ngIf='config.store.configSync.token')
|
||||
.alert.alert-danger(*ngIf='connectionSuccessful === false')
|
||||
i.fas.fa-exclamation-triangle
|
||||
|
@@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('Name for the new config')
|
||||
modal.componentInstance.value = name
|
||||
name = (await modal.result)?.value
|
||||
name = (await modal.result.catch(() => null))?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
@@ -141,4 +141,8 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
||||
this.platform.openExternal(this.config.store.configSync.host)
|
||||
}
|
||||
}
|
||||
|
||||
openTabbyWebInfo () {
|
||||
this.platform.openExternal('https://github.com/Eugeny/tabby-web')
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,32 @@
|
||||
.modal-header
|
||||
h3.m-0 {{group.name}}
|
||||
|
||||
.modal-body
|
||||
.row
|
||||
.col-12.col-lg-4
|
||||
.mb-3
|
||||
label(translate) Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
autofocus,
|
||||
[(ngModel)]='group.name',
|
||||
)
|
||||
|
||||
.col-12.col-lg-8
|
||||
.form-line.content-box
|
||||
.header
|
||||
.title(translate) Default profile group settings
|
||||
.description(translate) These apply to all profiles of a given type in this group
|
||||
|
||||
.list-group.mt-3.mb-3.content-box
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='editDefaults(provider)',
|
||||
*ngFor='let provider of providers'
|
||||
) {{provider.name|translate}}
|
||||
.me-auto
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||
i.fas.fa-trash-arrow-up
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-primary((click)='save()', translate) Save
|
||||
button.btn.btn-danger((click)='cancel()', translate) Cancel
|
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigProxy, ProfileGroup, Profile, ProfileProvider, PlatformService, TranslateService } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './editProfileGroupModal.component.pug',
|
||||
})
|
||||
export class EditProfileGroupModalComponent<G extends ProfileGroup> {
|
||||
@Input() group: G & ConfigProxy
|
||||
@Input() providers: ProfileProvider<Profile>[]
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal,
|
||||
private platform: PlatformService,
|
||||
private translate: TranslateService,
|
||||
) {}
|
||||
|
||||
save () {
|
||||
this.modalInstance.close({ group: this.group })
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
|
||||
editDefaults (provider: ProfileProvider<Profile>) {
|
||||
this.modalInstance.close({ group: this.group, provider })
|
||||
}
|
||||
|
||||
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Restore settings to inherited defaults ?'),
|
||||
buttons: [
|
||||
this.translate.instant('Delete'),
|
||||
this.translate.instant('Keep'),
|
||||
],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.group.defaults?.[provider.id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface EditProfileGroupModalComponentResult<G extends ProfileGroup> {
|
||||
group: G
|
||||
provider?: ProfileProvider<Profile>
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
.modal-header(*ngIf='!defaultsMode')
|
||||
.modal-header(*ngIf='defaultsMode === "disabled"')
|
||||
h3.m-0 {{profile.name}}
|
||||
|
||||
.modal-header(*ngIf='defaultsMode')
|
||||
.modal-header(*ngIf='defaultsMode !== "disabled"')
|
||||
h3.m-0(
|
||||
translate='Defaults for {type}',
|
||||
[translateParams]='{type: profileProvider.name}'
|
||||
@@ -10,7 +10,7 @@
|
||||
.modal-body
|
||||
.row
|
||||
.col-12.col-lg-4
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
@@ -18,17 +18,20 @@
|
||||
[(ngModel)]='profile.name',
|
||||
)
|
||||
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Group
|
||||
input.form-control(
|
||||
type='text',
|
||||
alwaysVisibleTypeahead,
|
||||
placeholder='Ungrouped',
|
||||
[(ngModel)]='profile.group',
|
||||
[(ngModel)]='profileGroup',
|
||||
[ngbTypeahead]='groupTypeahead',
|
||||
[inputFormatter]="groupFormatter",
|
||||
[resultFormatter]="groupFormatter",
|
||||
[editable]="false"
|
||||
)
|
||||
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Icon
|
||||
.input-group
|
||||
input.form-control(
|
||||
@@ -74,9 +77,15 @@
|
||||
)
|
||||
option(ngValue='auto', translate) Auto
|
||||
option(ngValue='keep', translate) Keep
|
||||
option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect
|
||||
option(*ngIf='isConnectable()', ngValue='reconnect', translate) Reconnect
|
||||
option(ngValue='close', translate) Close
|
||||
|
||||
|
||||
.form-line(*ngIf='isConnectable()')
|
||||
.header
|
||||
.title(translate) Clear terminal after connection
|
||||
toggle(
|
||||
[(ngModel)]='profile.clearServiceMessagesOnConnect',
|
||||
)
|
||||
.mb-4
|
||||
|
||||
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
|
||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
|
||||
import { ConfigProxy, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider } from 'tabby-core'
|
||||
|
||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||
const iconsClassList = Object.keys(iconsData).map(
|
||||
@@ -19,8 +19,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
@Input() profile: P & ConfigProxy
|
||||
@Input() profileProvider: ProfileProvider<P>
|
||||
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
|
||||
@Input() defaultsMode = false
|
||||
groupNames: string[]
|
||||
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
|
||||
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
|
||||
groups: PartialProfileGroup<ProfileGroup>[]
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
private _profile: Profile
|
||||
@@ -30,14 +31,14 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
private injector: Injector,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private profilesService: ProfilesService,
|
||||
config: ConfigService,
|
||||
private modalInstance: NgbActiveModal,
|
||||
) {
|
||||
this.groupNames = [...new Set(
|
||||
(config.store.profiles as Profile[])
|
||||
.map(x => x.group)
|
||||
.filter(x => !!x),
|
||||
)].sort() as string[]
|
||||
if (this.defaultsMode === 'disabled') {
|
||||
this.profilesService.getProfileGroups().then(groups => {
|
||||
this.groups = groups
|
||||
this.profileGroup = groups.find(g => g.id === this.profile.group)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
colorsAutocomplete = text$ => text$.pipe(
|
||||
@@ -56,7 +57,7 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
|
||||
ngOnInit () {
|
||||
this._profile = this.profile
|
||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode)
|
||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' })
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
@@ -72,13 +73,15 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
}
|
||||
}
|
||||
|
||||
groupTypeahead = (text$: Observable<string>) =>
|
||||
groupTypeahead: OperatorFunction<string, readonly PartialProfileGroup<ProfileGroup>[]> = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))),
|
||||
map(q => this.groups.filter(g => !q || g.name.toLowerCase().includes(q.toLowerCase()))),
|
||||
)
|
||||
|
||||
groupFormatter = (g: PartialProfileGroup<ProfileGroup>) => g.name
|
||||
|
||||
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
@@ -86,7 +89,12 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
)
|
||||
|
||||
save () {
|
||||
this.profile.group ||= undefined
|
||||
if (!this.profileGroup) {
|
||||
this.profile.group = undefined
|
||||
} else {
|
||||
this.profile.group = this.profileGroup.id
|
||||
}
|
||||
|
||||
this.settingsComponentInstance?.save?.()
|
||||
this.profile.__cleanup()
|
||||
this.modalInstance.close(this._profile)
|
||||
@@ -95,4 +103,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
|
||||
isConnectable (): boolean {
|
||||
return this.profileProvider instanceof ConnectableProfileProvider
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,14 +12,16 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
[(ngModel)]='config.store.terminal.profile',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(
|
||||
*ngFor='let profile of profiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
option(
|
||||
*ngFor='let profile of builtinProfiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
optgroup([label]='"Custom Profiles"|translate', *ngIf='customProfiles?.length > 0')
|
||||
option(
|
||||
*ngFor='let profile of customProfiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
optgroup([label]='"Built-in Profiles"|translate')
|
||||
option(
|
||||
*ngFor='let profile of builtinProfiles',
|
||||
[ngValue]='profile.id'
|
||||
) {{profile.name}}
|
||||
|
||||
.d-flex.mb-3
|
||||
.input-group
|
||||
@@ -27,9 +29,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
|
||||
|
||||
button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile
|
||||
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3
|
||||
button.btn.btn-primary(ngbDropdownToggle)
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New
|
||||
div(ngbDropdownMenu)
|
||||
button(ngbDropdownItem, (click)='newProfile()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile
|
||||
button(ngbDropdownItem, (click)='newProfileGroup()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile Group
|
||||
|
||||
.list-group.mt-3.mb-3
|
||||
ng-container(*ngFor='let group of profileGroups')
|
||||
@@ -37,17 +47,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='toggleGroupCollapse(group)'
|
||||
)
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0')
|
||||
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); editGroup(group)'
|
||||
(click)='$event.stopPropagation(); editProfileGroup(group)'
|
||||
)
|
||||
i.fas.fa-pencil-alt
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
||||
(click)='$event.stopPropagation(); deleteProfileGroup(group)'
|
||||
)
|
||||
i.fas.fa-trash-alt
|
||||
ng-container(*ngIf='!group.collapsed')
|
||||
@@ -67,7 +77,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
|
||||
.me-auto
|
||||
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); launchProfile(profile)')
|
||||
button.btn.btn-link.hover-reveal.ms-1(*ngIf='!profile.isTemplate', (click)='$event.stopPropagation(); launchProfile(profile)')
|
||||
i.fas.fa-play
|
||||
|
||||
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
|
||||
@@ -169,9 +179,12 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
.description(translate) These apply to all profiles of a given type
|
||||
|
||||
.list-group.mt-3.mb-3.content-box
|
||||
a.list-group-item.list-group-item-action(
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='editDefaults(provider)',
|
||||
*ngFor='let provider of profileProviders'
|
||||
) {{provider.name|translate}}
|
||||
.me-auto
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||
i.fas.fa-trash-arrow-up
|
||||
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
@@ -1,32 +1,29 @@
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import slugify from 'slugify'
|
||||
import deepClone from 'clone-deep'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
|
||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup, QuickConnectProfileProvider } from 'tabby-core'
|
||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||
|
||||
interface ProfileGroup {
|
||||
name?: string
|
||||
profiles: PartialProfile<Profile>[]
|
||||
editable: boolean
|
||||
collapsed: boolean
|
||||
}
|
||||
import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
|
||||
|
||||
_('Filter')
|
||||
_('Ungrouped')
|
||||
|
||||
interface CollapsableProfileGroup extends ProfileGroup {
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './profilesSettingsTab.component.pug',
|
||||
styleUrls: ['./profilesSettingsTab.component.scss'],
|
||||
})
|
||||
export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
profiles: PartialProfile<Profile>[] = []
|
||||
builtinProfiles: PartialProfile<Profile>[] = []
|
||||
profiles: PartialProfile<Profile>[] = []
|
||||
templateProfiles: PartialProfile<Profile>[] = []
|
||||
profileGroups: ProfileGroup[]
|
||||
customProfiles: PartialProfile<Profile>[] = []
|
||||
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[]
|
||||
filter = ''
|
||||
Platform = Platform
|
||||
|
||||
@@ -45,12 +42,17 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
}
|
||||
|
||||
async ngOnInit (): Promise<void> {
|
||||
this.refresh()
|
||||
await this.refreshProfileGroups()
|
||||
await this.refreshProfiles()
|
||||
this.subscribeUntilDestroyed(this.config.changed$, () => this.refreshProfileGroups())
|
||||
this.subscribeUntilDestroyed(this.config.changed$, () => this.refreshProfiles())
|
||||
}
|
||||
|
||||
async refreshProfiles (): Promise<void> {
|
||||
this.builtinProfiles = (await this.profilesService.getProfiles()).filter(x => x.isBuiltin)
|
||||
this.customProfiles = (await this.profilesService.getProfiles()).filter(x => !x.isBuiltin)
|
||||
this.templateProfiles = this.builtinProfiles.filter(x => x.isTemplate)
|
||||
this.builtinProfiles = this.builtinProfiles.filter(x => !x.isTemplate)
|
||||
this.refresh()
|
||||
this.subscribeUntilDestroyed(this.config.changed$, () => this.refresh())
|
||||
}
|
||||
|
||||
launchProfile (profile: PartialProfile<Profile>): void {
|
||||
@@ -59,7 +61,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
|
||||
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
|
||||
if (!base) {
|
||||
let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
||||
let profiles = await this.profilesService.getProfiles()
|
||||
profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
|
||||
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||
base = await this.selector.show(
|
||||
@@ -67,31 +69,32 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
profiles.map(p => ({
|
||||
icon: p.icon,
|
||||
description: this.profilesService.getDescription(p) ?? undefined,
|
||||
name: p.group ? `${p.group} / ${p.name}` : p.name,
|
||||
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name,
|
||||
result: p,
|
||||
})),
|
||||
)
|
||||
).catch(() => undefined)
|
||||
if (!base) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const profile: PartialProfile<Profile> = deepClone(base)
|
||||
delete profile.id
|
||||
const baseProfile: PartialProfile<Profile> = deepClone(base)
|
||||
delete baseProfile.id
|
||||
if (base.isTemplate) {
|
||||
profile.name = ''
|
||||
baseProfile.name = ''
|
||||
} else if (!base.isBuiltin) {
|
||||
profile.name = this.translate.instant('{name} copy', base)
|
||||
baseProfile.name = this.translate.instant('{name} copy', base)
|
||||
}
|
||||
profile.isBuiltin = false
|
||||
profile.isTemplate = false
|
||||
const result = await this.showProfileEditModal(profile)
|
||||
baseProfile.isBuiltin = false
|
||||
baseProfile.isTemplate = false
|
||||
const result = await this.showProfileEditModal(baseProfile)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
Object.assign(profile, result)
|
||||
if (!profile.name) {
|
||||
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
|
||||
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
||||
if (!result.name) {
|
||||
const cfgProxy = this.profilesService.getConfigProxyForProfile(result)
|
||||
result.name = this.profilesService.providerForProfile(result)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
||||
}
|
||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||
this.config.store.profiles = [profile, ...this.config.store.profiles]
|
||||
await this.profilesService.newProfile(result)
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
Object.assign(profile, result)
|
||||
await this.profilesService.writeProfile(result)
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
@@ -121,12 +124,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fully replace the config
|
||||
for (const k in profile) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profile[k]
|
||||
}
|
||||
|
||||
result.type = provider.id
|
||||
return result
|
||||
}
|
||||
@@ -144,69 +141,79 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
this.profilesService.providerForProfile(profile)?.deleteProfile(
|
||||
this.profilesService.getConfigProxyForProfile(profile))
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
|
||||
const profileHotkeyName = AppHotkeyProvider.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
await this.profilesService.deleteProfile(profile)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
refresh (): void {
|
||||
this.profiles = this.config.store.profiles
|
||||
this.profileGroups = []
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
|
||||
for (const profile of this.profiles) {
|
||||
// Group null, undefined and empty together
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let group = this.profileGroups.find(x => x.name === (profile.group || ''))
|
||||
if (!group) {
|
||||
group = {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: profile.group || '',
|
||||
profiles: [],
|
||||
editable: true,
|
||||
collapsed: profileGroupCollapsed[profile.group ?? ''] ?? false,
|
||||
}
|
||||
this.profileGroups.push(group)
|
||||
}
|
||||
group.profiles.push(profile)
|
||||
}
|
||||
|
||||
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
|
||||
|
||||
const builtIn = {
|
||||
name: this.translate.instant('Built-in'),
|
||||
profiles: this.builtinProfiles,
|
||||
editable: false,
|
||||
collapsed: false,
|
||||
}
|
||||
builtIn.collapsed = profileGroupCollapsed[builtIn.name ?? ''] ?? false
|
||||
this.profileGroups.push(builtIn)
|
||||
}
|
||||
|
||||
async editGroup (group: ProfileGroup): Promise<void> {
|
||||
async newProfileGroup (): Promise<void> {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('New name')
|
||||
modal.componentInstance.value = group.name
|
||||
const result = await modal.result
|
||||
if (result) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
profile.group = result.value
|
||||
}
|
||||
this.config.store.profiles = this.profiles
|
||||
modal.componentInstance.prompt = this.translate.instant('New group name')
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result?.value.trim()) {
|
||||
await this.profilesService.newProfileGroup({ id: '', name: result.value })
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGroup (group: ProfileGroup): Promise<void> {
|
||||
async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> {
|
||||
const result = await this.showProfileGroupEditModal(group)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(result))
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
async showProfileGroupEditModal (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||
const modal = this.ngbModal.open(
|
||||
EditProfileGroupModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
|
||||
modal.componentInstance.group = deepClone(group)
|
||||
modal.componentInstance.providers = this.profileProviders
|
||||
|
||||
const result: EditProfileGroupModalComponentResult<CollapsableProfileGroup> | null = await modal.result.catch(() => null)
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (result.provider) {
|
||||
return this.editProfileGroupDefaults(result.group, result.provider)
|
||||
}
|
||||
|
||||
return result.group
|
||||
}
|
||||
|
||||
private async editProfileGroupDefaults (group: PartialProfileGroup<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||
const modal = this.ngbModal.open(
|
||||
EditProfileModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
const model = group.defaults?.[provider.id] ?? {}
|
||||
model.type = provider.id
|
||||
modal.componentInstance.profile = Object.assign({}, model)
|
||||
modal.componentInstance.profileProvider = provider
|
||||
modal.componentInstance.defaultsMode = 'group'
|
||||
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result) {
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
}
|
||||
Object.assign(model, result)
|
||||
if (!group.defaults) {
|
||||
group.defaults = {}
|
||||
}
|
||||
group.defaults[provider.id] = model
|
||||
}
|
||||
return this.showProfileGroupEditModal(group)
|
||||
}
|
||||
|
||||
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
@@ -219,7 +226,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
if ((await this.platform.showMessageBox(
|
||||
let deleteProfiles = false
|
||||
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Delete the group\'s profiles?'),
|
||||
@@ -230,19 +238,26 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
},
|
||||
)).response === 0) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
delete profile.group
|
||||
}
|
||||
} else {
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
|
||||
)).response !== 0) {
|
||||
deleteProfiles = true
|
||||
}
|
||||
|
||||
await this.profilesService.deleteProfileGroup(group, { deleteProfiles })
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
isGroupVisible (group: ProfileGroup): boolean {
|
||||
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
|
||||
async refreshProfileGroups (): Promise<void> {
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
const groups = await this.profilesService.getProfileGroups({ includeNonUserGroup: true, includeProfiles: true })
|
||||
groups.sort((a, b) => a.name.localeCompare(b.name))
|
||||
groups.sort((a, b) => (a.id === 'built-in' || !a.editable ? 1 : 0) - (b.id === 'built-in' || !b.editable ? 1 : 0))
|
||||
groups.sort((a, b) => (a.id === 'ungrouped' ? 0 : 1) - (b.id === 'ungrouped' ? 0 : 1))
|
||||
this.profileGroups = groups.map(g => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false))
|
||||
}
|
||||
|
||||
isGroupVisible (group: PartialProfileGroup<ProfileGroup>): boolean {
|
||||
return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x))
|
||||
}
|
||||
|
||||
isProfileVisible (profile: PartialProfile<Profile>): boolean {
|
||||
@@ -270,11 +285,12 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||
}
|
||||
|
||||
toggleGroupCollapse (group: ProfileGroup): void {
|
||||
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||
if (group.profiles?.length === 0) {
|
||||
return
|
||||
}
|
||||
group.collapsed = !group.collapsed
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
profileGroupCollapsed[group.name ?? ''] = group.collapsed
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
this.saveProfileGroupCollapse(group)
|
||||
}
|
||||
|
||||
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
@@ -282,21 +298,40 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
EditProfileModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
const model = this.config.store.profileDefaults[provider.id] ?? {}
|
||||
const model = this.profilesService.getProviderDefaults(provider)
|
||||
model.type = provider.id
|
||||
modal.componentInstance.profile = Object.assign({}, model)
|
||||
modal.componentInstance.profileProvider = provider
|
||||
modal.componentInstance.defaultsMode = true
|
||||
const result = await modal.result
|
||||
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
modal.componentInstance.defaultsMode = 'enabled'
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result) {
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
}
|
||||
Object.assign(model, result)
|
||||
this.profilesService.setProviderDefaults(provider, model)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Restore settings to defaults ?'),
|
||||
buttons: [
|
||||
this.translate.instant('Delete'),
|
||||
this.translate.instant('Keep'),
|
||||
],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
this.profilesService.setProviderDefaults(provider, {})
|
||||
await this.config.save()
|
||||
}
|
||||
Object.assign(model, result)
|
||||
this.config.store.profileDefaults[provider.id] = model
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
blacklistProfile (profile: PartialProfile<Profile>): void {
|
||||
@@ -314,6 +349,29 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
}
|
||||
|
||||
getQuickConnectProviders (): ProfileProvider<Profile>[] {
|
||||
return this.profileProviders.filter(x => x.supportsQuickConnect)
|
||||
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save ProfileGroup collapse state in localStorage
|
||||
*/
|
||||
private saveProfileGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
profileGroupCollapsed[group.id] = group.collapsed
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
}
|
||||
|
||||
private static collapsableIntoPartialProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): PartialProfileGroup<ProfileGroup> {
|
||||
const g: any = { ...group }
|
||||
delete g.collapsed
|
||||
return g
|
||||
}
|
||||
|
||||
private static intoPartialCollapsableProfileGroup (group: PartialProfileGroup<ProfileGroup>, collapsed: boolean): PartialProfileGroup<CollapsableProfileGroup> {
|
||||
const collapsableGroup = {
|
||||
...group,
|
||||
collapsed,
|
||||
}
|
||||
return collapsableGroup
|
||||
}
|
||||
}
|
||||
|
15
tabby-settings/src/components/showSecretModal.component.pug
Normal file
15
tabby-settings/src/components/showSecretModal.component.pug
Normal file
@@ -0,0 +1,15 @@
|
||||
h4.modal-header.m-0.pb-0 {{title}}
|
||||
.modal-body
|
||||
.input-group.w-100
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='secret.value',
|
||||
disabled
|
||||
)
|
||||
button.btn.btn-secondary(
|
||||
(click)='copySecret()'
|
||||
)
|
||||
i.fas.fa-copy
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-primary((click)='close()', translate) Close
|
27
tabby-settings/src/components/showSecretModal.component.ts
Normal file
27
tabby-settings/src/components/showSecretModal.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NotificationsService, VaultFileSecret } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './showSecretModal.component.pug',
|
||||
})
|
||||
export class ShowSecretModalComponent {
|
||||
@Input() title: string
|
||||
@Input() secret: VaultFileSecret
|
||||
|
||||
constructor (
|
||||
public modalInstance: NgbActiveModal,
|
||||
private notifications: NotificationsService,
|
||||
) { }
|
||||
|
||||
close (): void {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
|
||||
copySecret (): void {
|
||||
navigator.clipboard.writeText(this.secret.value)
|
||||
// Show a notification
|
||||
this.notifications.info('Copied to clipboard')
|
||||
}
|
||||
}
|
@@ -32,6 +32,9 @@ div(*ngIf='vault.isEnabled()')
|
||||
button.btn.btn-link(ngbDropdownToggle)
|
||||
i.fas.fa-ellipsis-v
|
||||
div(ngbDropdownMenu)
|
||||
button(ngbDropdownItem, (click)='showSecret(secret)')
|
||||
i.fas.fa-fw.fa-eye
|
||||
span(translate) Show
|
||||
button(
|
||||
ngbDropdownItem,
|
||||
*ngIf='secret.type === VAULT_SECRET_TYPE_FILE',
|
||||
|
@@ -3,6 +3,7 @@ import { Component, HostBinding } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BaseComponent, VaultService, VaultSecret, Vault, PlatformService, ConfigService, VAULT_SECRET_TYPE_FILE, PromptModalComponent, VaultFileSecret, TranslateService } from 'tabby-core'
|
||||
import { SetVaultPassphraseModalComponent } from './setVaultPassphraseModal.component'
|
||||
import { ShowSecretModalComponent } from './showSecretModal.component'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@@ -35,9 +36,11 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||
|
||||
async enableVault () {
|
||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||
const newPassphrase = await modal.result
|
||||
await this.vault.setEnabled(true, newPassphrase)
|
||||
this.vaultContents = await this.vault.load(newPassphrase)
|
||||
const newPassphrase = await modal.result.catch(() => null)
|
||||
if (newPassphrase) {
|
||||
await this.vault.setEnabled(true, newPassphrase)
|
||||
this.vaultContents = await this.vault.load(newPassphrase)
|
||||
}
|
||||
}
|
||||
|
||||
async disableVault () {
|
||||
@@ -65,8 +68,10 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||
return
|
||||
}
|
||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||
const newPassphrase = await modal.result
|
||||
this.vault.save(this.vaultContents, newPassphrase)
|
||||
const newPassphrase = await modal.result.catch(() => null)
|
||||
if (newPassphrase) {
|
||||
this.vault.save(this.vaultContents, newPassphrase)
|
||||
}
|
||||
}
|
||||
|
||||
async toggleConfigEncrypted () {
|
||||
@@ -93,6 +98,16 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||
return this.translate.instant('Unknown secret of type {type} for {key}', { type: secret.type, key: JSON.stringify(secret.key) })
|
||||
}
|
||||
|
||||
showSecret (secret: VaultSecret) {
|
||||
if (!this.vaultContents) {
|
||||
return
|
||||
}
|
||||
const modal = this.ngbModal.open(ShowSecretModalComponent)
|
||||
modal.componentInstance.title = this.getSecretLabel(secret)
|
||||
modal.componentInstance.secret = secret
|
||||
|
||||
}
|
||||
|
||||
removeSecret (secret: VaultSecret) {
|
||||
if (!this.vaultContents) {
|
||||
return
|
||||
@@ -118,7 +133,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||
modal.componentInstance.prompt = this.translate.instant('New name')
|
||||
modal.componentInstance.value = secret.key.description
|
||||
|
||||
const description = (await modal.result)?.value
|
||||
const description = (await modal.result.catch(() => null))?.value
|
||||
if (!description) {
|
||||
return
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@ import { ConfigProvider, Platform } from 'tabby-core'
|
||||
export class SettingsConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
configSync: {
|
||||
host: 'https://api.tabby.sh',
|
||||
token: '',
|
||||
host: null,
|
||||
token: null,
|
||||
configID: null,
|
||||
auto: false,
|
||||
parts: {
|
||||
@@ -20,6 +20,7 @@ export class SettingsConfigProvider extends ConfigProvider {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
platformDefaults = {
|
||||
[Platform.macOS]: {
|
||||
hotkeys: {
|
||||
|
@@ -7,6 +7,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'
|
||||
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
|
||||
|
||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||
import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component'
|
||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||
@@ -18,6 +19,7 @@ import { SetVaultPassphraseModalComponent } from './components/setVaultPassphras
|
||||
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
|
||||
import { ReleaseNotesComponent } from './components/releaseNotesTab.component'
|
||||
import { ConfigSyncSettingsTabComponent } from './components/configSyncSettingsTab.component'
|
||||
import { ShowSecretModalComponent } from './components/showSecretModal.component'
|
||||
|
||||
import { ConfigSyncService } from './services/configSync.service'
|
||||
|
||||
@@ -48,6 +50,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
||||
],
|
||||
declarations: [
|
||||
EditProfileModalComponent,
|
||||
EditProfileGroupModalComponent,
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
MultiHotkeyInputComponent,
|
||||
@@ -59,6 +62,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
||||
WindowSettingsTabComponent,
|
||||
ConfigSyncSettingsTabComponent,
|
||||
ReleaseNotesComponent,
|
||||
ShowSecretModalComponent,
|
||||
],
|
||||
})
|
||||
export default class SettingsModule {
|
||||
|
@@ -2,15 +2,15 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/marked@^4.0.8":
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.8.tgz#b316887ab3499d0a8f4c70b7bd8508f92d477955"
|
||||
integrity sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==
|
||||
"@types/marked@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-5.0.1.tgz#15acd796d722b91bf00738c8c8539aaf5034f0c6"
|
||||
integrity sha512-Y3pAUzHKh605fN6fvASsz5FDSWbZcs/65Q6xYRmnIP9ZIYz27T4IOmXfH9gWJV1dpi7f1e7z7nBGUTx/a0ptpA==
|
||||
|
||||
marked@^4.2.12:
|
||||
version "4.2.12"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.12.tgz#d69a64e21d71b06250da995dcd065c11083bebb5"
|
||||
integrity sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==
|
||||
marked@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-5.1.2.tgz#62b5ccfc75adf72ca3b64b2879b551d89e77677f"
|
||||
integrity sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==
|
||||
|
||||
ngx-infinite-scroll@^16:
|
||||
version "16.0.0"
|
||||
|
@@ -25,6 +25,7 @@
|
||||
"@types/node": "20.3.1",
|
||||
"@types/ssh2": "^0.5.46",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"diffie-hellman": "^5.0.3",
|
||||
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
|
||||
"strip-ansi": "^7.0.0"
|
||||
},
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { BaseTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
||||
import { ConnectableTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
||||
|
||||
export enum SSHAlgorithmType {
|
||||
HMAC = 'hmac',
|
||||
@@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
|
||||
HOSTKEY = 'serverHostKey',
|
||||
}
|
||||
|
||||
export interface SSHProfile extends BaseTerminalProfile {
|
||||
export interface SSHProfile extends ConnectableTerminalProfile {
|
||||
options: SSHProfileOptions
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user