Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
e0fb98bfc6 build(deps): bump patch-package from 6.5.0 to 8.0.0 in /app
Bumps [patch-package](https://github.com/ds300/patch-package) from 6.5.0 to 8.0.0.
- [Release notes](https://github.com/ds300/patch-package/releases)
- [Changelog](https://github.com/ds300/patch-package/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ds300/patch-package/commits)

---
updated-dependencies:
- dependency-name: patch-package
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-03 08:25:44 +00:00
121 changed files with 5921 additions and 9730 deletions

View File

@@ -1220,24 +1220,6 @@
"contributions": [ "contributions": [
"code" "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"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

1
.env
View File

@@ -1 +0,0 @@
# TABBY_CONFIG_DIRECTORY="PATH_TO_DIRECTORY"

View File

@@ -1,13 +1,7 @@
settings: settings:
import/parsers:
'@typescript-eslint/parser': ['.ts']
import/resolver: import/resolver:
typescript: typescript: true
project:
- tsconfig.json
- tabby-*/tsconfig.json
node: true node: true
env: env:
browser: true browser: true
es6: true es6: true
@@ -34,7 +28,7 @@ overrides:
- plugin:import/typescript - plugin:import/typescript
plugins: plugins:
- '@typescript-eslint' - '@typescript-eslint'
- import - 'import'
rules: rules:
'@typescript-eslint/semi': '@typescript-eslint/semi':
- error - error
@@ -136,7 +130,6 @@ overrides:
'@typescript-eslint/naming-convention': off '@typescript-eslint/naming-convention': off
'@typescript-eslint/lines-between-class-members': '@typescript-eslint/lines-between-class-members':
- error - error
- always
- exceptAfterSingleLine: true - exceptAfterSingleLine: true
'@typescript-eslint/dot-notation': off '@typescript-eslint/dot-notation': off
'@typescript-eslint/no-implicit-any-catch': off '@typescript-eslint/no-implicit-any-catch': off
@@ -159,6 +152,3 @@ overrides:
'@typescript-eslint/consistent-generic-constructors': off '@typescript-eslint/consistent-generic-constructors': off
'keyword-spacing': off 'keyword-spacing': off
'@typescript-eslint/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

View File

@@ -110,16 +110,16 @@ jobs:
- name: Package artifacts - name: Package artifacts
run: | run: |
mkdir artifact-dmg mkdir artifact-pkg
mv dist/*.dmg artifact-dmg/ mv dist/*.pkg artifact-pkg/
mkdir artifact-zip mkdir artifact-zip
mv dist/*.zip artifact-zip/ mv dist/*.zip artifact-zip/
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
name: Upload DMG name: Upload PKG
with: with:
name: macOS .dmg (${{matrix.arch}}) name: macOS .pkg (${{matrix.arch}})
path: artifact-dmg path: artifact-pkg
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
name: Upload ZIP name: Upload ZIP

View File

@@ -119,7 +119,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
@@ -327,10 +326,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -120,7 +120,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
# Temas # Temas
@@ -329,10 +328,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -120,7 +120,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
@@ -326,10 +325,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -117,7 +117,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
# Temi # Temi
@@ -322,10 +321,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -127,7 +127,6 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
* [clippy](https://github.com/Eugeny/tabby-clippy) - プラグインの作例として、いつも厄介なあいつが出てくるプラグイン * [clippy](https://github.com/Eugeny/tabby-clippy) - プラグインの作例として、いつも厄介なあいつが出てくるプラグイン
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 指定された設定からカスタマイズされたワークスペースを作成することができます * [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 指定された設定からカスタマイズされたワークスペースを作成することができます
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby内の端末で選択したテキストを既定ブラウザで開くことができます。 * [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> <a name="themes"></a>
@@ -337,10 +336,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -111,7 +111,6 @@
* [clippy](https://github.com/Eugeny/tabby-clippy) - 항상 당신을 귀찮게 하는 예제 플러그인 * [clippy](https://github.com/Eugeny/tabby-clippy) - 항상 당신을 귀찮게 하는 예제 플러그인
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 주어진 구성을 기반으로 사용자 정의 작업 공간 프로필을 생성할 수 있습니다 * [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 주어진 구성을 기반으로 사용자 정의 작업 공간 프로필을 생성할 수 있습니다
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby의 탭에서 선택한 텍스트로 기본 시스템 브라우저를 엽니다 * [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> <a name="themes"></a>
# 테마 # 테마
@@ -316,10 +315,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -128,7 +128,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
@@ -139,7 +138,6 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
* [gruvbox](https://github.com/porkloin/terminus-theme-gruvbox) * [gruvbox](https://github.com/porkloin/terminus-theme-gruvbox)
* [windows10](https://www.npmjs.com/package/terminus-theme-windows10) * [windows10](https://www.npmjs.com/package/terminus-theme-windows10)
* [altair](https://github.com/yxuko/terminus-altair) * [altair](https://github.com/yxuko/terminus-altair)
* [catppuccin](https://github.com/catppuccin/tabby) - Soothing pastel theme for Tabby
# Sponsors <!-- omit in toc --> # Sponsors <!-- omit in toc -->
@@ -339,10 +337,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -120,7 +120,6 @@ 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 * [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 * [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 * [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> <a name="themes"></a>
@@ -330,10 +329,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -117,7 +117,6 @@
* [clippy](https://github.com/Eugeny/tabby-clippy) — плагин-пример, который постоянно будет вас бесить; * [clippy](https://github.com/Eugeny/tabby-clippy) — плагин-пример, который постоянно будет вас бесить;
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) — позволяет создавать пользовательские провили рабочего окружеиня на основе конфига; * [workspace-manager](https://github.com/composer404/tabby-workspace-manager) — позволяет создавать пользовательские провили рабочего окружеиня на основе конфига;
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) — открывает браузер по умолчанию с текстом, выделенном во вкладке Tabby. * [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> <a name="themes"></a>
# Темы # Темы
@@ -322,10 +321,6 @@ 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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -116,7 +116,6 @@
* [clippy](https://github.com/Eugeny/tabby-clippy) - 一个可以一直烦你的示例插件 * [clippy](https://github.com/Eugeny/tabby-clippy) - 一个可以一直烦你的示例插件
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 允许根据给定的配置创建自定义工作区配置文件 * [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 允许根据给定的配置创建自定义工作区配置文件
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - 从 Tabby 标签页带有选中的文本来打开系统默认浏览器 * [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> <a name="themes"></a>
# 主题 # 主题
@@ -321,10 +320,6 @@
<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/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> <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>
<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>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -183,7 +183,7 @@ export class Application {
} }
enableTray (): void { enableTray (): void {
if (!!this.tray || process.platform === 'linux') { if (this.tray || process.platform === 'linux') {
return return
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {

View File

@@ -1,14 +1,13 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import * as yaml from 'js-yaml' import * as yaml from 'js-yaml'
import { app } from 'electron'
import { writeFile } from 'atomically' 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 { 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) && ( if (fs.existsSync(legacyConfigPath) && (
!fs.existsSync(configPath) || !fs.existsSync(configPath) ||
fs.statSync(configPath).mtime < fs.statSync(legacyConfigPath).mtime fs.statSync(configPath).mtime < fs.statSync(legacyConfigPath).mtime
@@ -20,6 +19,7 @@ export function migrateConfig (): void {
export function loadConfig (): any { export function loadConfig (): any {
migrateConfig() migrateConfig()
const configPath = path.join(app.getPath('userData'), 'config.yaml')
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
return yaml.load(fs.readFileSync(configPath, 'utf8')) return yaml.load(fs.readFileSync(configPath, 'utf8'))
} else { } else {
@@ -27,6 +27,8 @@ export function loadConfig (): any {
} }
} }
const configPath = path.join(app.getPath('userData'), 'config.yaml')
export async function saveConfig (content: string): Promise<void> { export async function saveConfig (content: string): Promise<void> {
await writeFile(configPath, content, { encoding: 'utf8' }) await writeFile(configPath, content, { encoding: 'utf8' })
await writeFile(configPath + '.backup', content, { encoding: 'utf8' }) await writeFile(configPath + '.backup', content, { encoding: 'utf8' })

View File

@@ -1,21 +1,17 @@
import { app, ipcMain, Menu, dialog } from 'electron'
// 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 'v8-compile-cache'
import './portable' import './portable'
import 'source-map-support/register' import 'source-map-support/register'
import './sentry' import './sentry'
import './lru' import './lru'
import { app, ipcMain, Menu, dialog } from 'electron'
import { parseArgs } from './cli' import { parseArgs } from './cli'
import { Application } from './app' import { Application } from './app'
import electronDebug = require('electron-debug') import electronDebug = require('electron-debug')
import { loadConfig } from './config' import { loadConfig } from './config'
if (!process.env.TABBY_PLUGINS) {
process.env.TABBY_PLUGINS = ''
}
const argv = parseArgs(process.argv, process.cwd()) const argv = parseArgs(process.argv, process.cwd())

View File

@@ -392,15 +392,14 @@ export class Window {
return return
} }
if (process.platform === 'win32') { const symbolColor: string = theme.foreground
const symbolColor: string = theme.foreground
this.window.setTitleBarOverlay( this.window.setTitleBarOverlay(
{ {
symbolColor: symbolColor, symbolColor: symbolColor,
height: 32, height: 32,
}, },
) )
}
}) })
ipcMain.on('window-set-title', (event, title) => { ipcMain.on('window-set-title', (event, title) => {

View File

@@ -37,7 +37,7 @@
"optionalDependencies": { "optionalDependencies": {
"@tabby-gang/windows-blurbehind": "^3.0.0", "@tabby-gang/windows-blurbehind": "^3.0.0",
"macos-native-processlist": "^2.1.0", "macos-native-processlist": "^2.1.0",
"patch-package": "^6.5.0", "patch-package": "^8.0.0",
"serialport": "11.0.1", "serialport": "11.0.1",
"serialport-binding-webserialapi": "^1.0.3", "serialport-binding-webserialapi": "^1.0.3",
"windows-native-registry": "^3.2.1", "windows-native-registry": "^3.2.1",

View File

@@ -327,6 +327,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atomically@^2.0.2: atomically@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/atomically/-/atomically-2.0.2.tgz#e5a6e8021441405b7a1c36d4587e25f7a13545f2" resolved "https://registry.yarnpkg.com/atomically/-/atomically-2.0.2.tgz#e5a6e8021441405b7a1c36d4587e25f7a13545f2"
@@ -538,6 +543,11 @@ ci-info@^2.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.7.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
cidr-regex@^2.0.10: cidr-regex@^2.0.10:
version "2.0.10" version "2.0.10"
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.10.tgz#af13878bd4ad704de77d6dc800799358b3afa70d" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.10.tgz#af13878bd4ad704de77d6dc800799358b3afa70d"
@@ -741,16 +751,14 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^6.0.5: cross-spawn@^7.0.3:
version "6.0.5" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies: dependencies:
nice-try "^1.0.4" path-key "^3.1.0"
path-key "^2.0.1" shebang-command "^2.0.0"
semver "^5.5.0" which "^2.0.1"
shebang-command "^1.2.0"
which "^1.2.9"
crypto-random-string@^1.0.0: crypto-random-string@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -1206,14 +1214,15 @@ fs-extra@^10.0.0:
jsonfile "^6.0.1" jsonfile "^6.0.1"
universalify "^2.0.0" universalify "^2.0.0"
fs-extra@^7.0.1: fs-extra@^9.0.0:
version "7.0.1" version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies: dependencies:
graceful-fs "^4.1.2" at-least-node "^1.0.0"
jsonfile "^4.0.0" graceful-fs "^4.2.0"
universalify "^0.1.0" jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^1.2.7: fs-minipass@^1.2.7:
version "1.2.7" version "1.2.7"
@@ -1574,13 +1583,6 @@ is-ci@^1.0.10:
dependencies: dependencies:
ci-info "^1.5.0" ci-info "^1.5.0"
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-cidr@^3.0.0: is-cidr@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-3.1.1.tgz#e92ef121bdec2782271a77ce487a8b8df3718ab7" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-3.1.1.tgz#e92ef121bdec2782271a77ce487a8b8df3718ab7"
@@ -1755,18 +1757,18 @@ json-schema@0.2.3:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-stable-stringify@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0"
integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==
dependencies:
jsonify "^0.0.1"
json-stringify-safe@~5.0.1: json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1: jsonfile@^6.0.1:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -1776,6 +1778,11 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
jsonify@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
jsonparse@^1.2.0: jsonparse@^1.2.0:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -2283,11 +2290,6 @@ ngx-filesize@^3.0.2:
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-abi@^2.20.0: node-abi@^2.20.0:
version "2.30.1" version "2.30.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf"
@@ -2823,25 +2825,26 @@ parse-json@^2.2.0:
dependencies: dependencies:
error-ex "^1.2.0" error-ex "^1.2.0"
patch-package@^6.5.0: patch-package@^8.0.0:
version "6.5.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.5.0.tgz#feb058db56f0005da59cfa316488321de585e88a" resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
integrity sha512-tC3EqJmo74yKqfsMzELaFwxOAu6FH6t+FzFOsnWAuARm7/n2xB5AOeOueE221eM9gtMuIKMKpF9tBy/X2mNP0Q== integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
dependencies: dependencies:
"@yarnpkg/lockfile" "^1.1.0" "@yarnpkg/lockfile" "^1.1.0"
chalk "^4.1.2" chalk "^4.1.2"
cross-spawn "^6.0.5" ci-info "^3.7.0"
cross-spawn "^7.0.3"
find-yarn-workspace-root "^2.0.0" find-yarn-workspace-root "^2.0.0"
fs-extra "^7.0.1" fs-extra "^9.0.0"
is-ci "^2.0.0" json-stable-stringify "^1.0.2"
klaw-sync "^6.0.0" klaw-sync "^6.0.0"
minimist "^1.2.6" minimist "^1.2.6"
open "^7.4.2" open "^7.4.2"
rimraf "^2.6.3" rimraf "^2.6.3"
semver "^5.6.0" semver "^7.5.3"
slash "^2.0.0" slash "^2.0.0"
tmp "^0.0.33" tmp "^0.0.33"
yaml "^1.10.2" yaml "^2.2.2"
path-exists@^3.0.0: path-exists@^3.0.0:
version "3.0.0" version "3.0.0"
@@ -2858,11 +2861,16 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2:
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
path-key@^2.0.0, path-key@^2.0.1: path-key@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6: path-parse@^1.0.6:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
@@ -3275,15 +3283,15 @@ semver-diff@^2.0.0:
dependencies: dependencies:
semver "^5.0.3" semver "^5.0.3"
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1:
version "5.7.1" version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^7.3.5: semver@^7.3.5, semver@^7.5.3:
version "7.3.5" version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
@@ -3341,11 +3349,23 @@ shebang-command@^1.2.0:
dependencies: dependencies:
shebang-regex "^1.0.0" shebang-regex "^1.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^1.0.0: shebang-regex@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^3.0.0, signal-exit@^3.0.2: signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz"
@@ -3808,11 +3828,6 @@ unique-string@^1.0.0:
dependencies: dependencies:
crypto-random-string "^1.0.0" crypto-random-string "^1.0.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0: universalify@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
@@ -3933,6 +3948,13 @@ which@^1.2.9, which@^1.3.0, which@^1.3.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0: wide-align@^1.1.0:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz"
@@ -4055,10 +4077,10 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.2: yaml@^2.2.2:
version "1.10.2" version "2.3.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
yargs-parser@^15.0.1: yargs-parser@^15.0.1:
version "15.0.1" version "15.0.1"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,8 @@
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/webpack-env": "^1.18.0", "@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^5.54.1",
"apply-loader": "2.0.0", "apply-loader": "2.0.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
@@ -44,11 +44,11 @@
"electron-download": "^4.1.1", "electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0", "electron-installer-snap": "^5.1.0",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"eslint": "^8.48.0", "eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.27.5",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"gettext-extractor": "^3.8.0", "gettext-extractor": "^3.5.4",
"graceful-fs": "^4.2.10", "graceful-fs": "^4.2.10",
"html-loader": "4.2.0", "html-loader": "4.2.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
@@ -76,7 +76,7 @@
"source-code-pro": "^2.38.0", "source-code-pro": "^2.38.0",
"source-map-loader": "^4.0.1", "source-map-loader": "^4.0.1",
"source-sans-pro": "3.6.0", "source-sans-pro": "3.6.0",
"ssh2": "^1.14.0", "ssh2": "Eugeny/ssh2#9de907d62907d6d45debdcc0ed8dda5b7b19dc7c",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"thenby": "^1.3.4", "thenby": "^1.3.4",
@@ -95,7 +95,7 @@
}, },
"resolutions": { "resolutions": {
"*/pug": "^3", "*/pug": "^3",
"lzma-native": "^8.0.6", "lzma-native": "^8.0.0",
"*/node-abi": "^3.33.0", "*/node-abi": "^3.33.0",
"**/graceful-fs": "^4.2.4", "**/graceful-fs": "^4.2.4",
"nan": "2.17.0" "nan": "2.17.0"
@@ -115,8 +115,5 @@
"i18n:push": "crowdin push" "i18n:push": "crowdin push"
}, },
"type": "module", "type": "module",
"private": true, "private": true
"dependencies": {
"dotenv": "^16.3.1"
}
} }

View File

@@ -18,7 +18,7 @@ process.env.APPLE_APP_SPECIFIC_PASSWORD ??= process.env.APPSTORE_PASSWORD
builder({ builder({
dir: true, dir: true,
mac: ['dmg', 'zip'], mac: ['pkg', 'zip'],
x64: process.env.ARCH === 'x86_64', x64: process.env.ARCH === 'x86_64',
arm64: process.env.ARCH === 'arm64', arm64: process.env.ARCH === 'arm64',
config: { config: {

View File

@@ -16,7 +16,7 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow' export { HostWindowService } from './hostWindow'
export { HostAppService, Platform } from './hostApp' export { HostAppService, Platform } from './hostApp'
export { FileProvider } from './fileProvider' export { FileProvider } from './fileProvider'
export { ProfileProvider, ConnectableProfileProvider, QuickConnectProfileProvider, Profile, ConnectableProfile, PartialProfile, ProfileSettingsComponent, ProfileGroup, PartialProfileGroup } from './profileProvider' export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
export { PromptModalComponent } from '../components/promptModal.component' export { PromptModalComponent } from '../components/promptModal.component'
export * from './commands' export * from './commands'

View File

@@ -1,5 +1,5 @@
export interface MenuItemOptions { export interface MenuItemOptions {
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio' type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio')
label?: string label?: string
sublabel?: string sublabel?: string
enabled?: boolean enabled?: boolean

View File

@@ -21,10 +21,6 @@ export interface Profile {
isTemplate: boolean isTemplate: boolean
} }
export interface ConnectableProfile extends Profile {
clearServiceMessagesOnConnect: boolean
}
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
[K in keyof T]?: T[K] [K in keyof T]?: T[K]
}, 'options'>, 'type'>, 'name'> & { }, 'options'>, 'type'>, 'name'> & {
@@ -35,21 +31,6 @@ 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> { export interface ProfileSettingsComponent<P extends Profile> {
profile: P profile: P
save?: () => void save?: () => void
@@ -58,6 +39,7 @@ export interface ProfileSettingsComponent<P extends Profile> {
export abstract class ProfileProvider<P extends Profile> { export abstract class ProfileProvider<P extends Profile> {
id: string id: string
name: string name: string
supportsQuickConnect = false
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P> settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
configDefaults = {} configDefaults = {}
@@ -71,15 +53,13 @@ export abstract class ProfileProvider<P extends Profile> {
abstract getDescription (profile: PartialProfile<P>): string 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 { } 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
}

View File

@@ -18,7 +18,7 @@ export class CoreCommandProvider extends CommandProvider {
} }
async activate () { async activate () {
const profile = await this.profilesService.showProfileSelector().catch(() => null) const profile = await this.profilesService.showProfileSelector()
if (profile) { if (profile) {
this.profilesService.launchProfile(profile) this.profilesService.launchProfile(profile)
} }

View File

@@ -35,7 +35,8 @@ title-bar(
[@animateTab]='{value: "in", params: {size: targetTabSize}}', [@animateTab]='{value: "in", params: {size: targetTabSize}}',
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations', [@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
(click)='app.selectTab(tab)', (click)='app.selectTab(tab)',
[class.fully-draggable]='hostApp.platform != Platform.macOS' [class.fully-draggable]='hostApp.platform != Platform.macOS',
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
) )
.btn-group.background .btn-group.background
@@ -64,7 +65,7 @@ title-bar(
(transfersChange)='onTransfersChange()' (transfersChange)='onTransfersChange()'
) )
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin"') .drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
.btn-group.background .btn-group.background
.d-flex( .d-flex(

View File

@@ -75,7 +75,6 @@ export abstract class BaseTabComponent extends BaseComponent {
private titleChange = new Subject<string>() private titleChange = new Subject<string>()
private focused = new Subject<void>() private focused = new Subject<void>()
private blurred = new Subject<void>() private blurred = new Subject<void>()
private visibility = new Subject<boolean>()
private progress = new Subject<number|null>() private progress = new Subject<number|null>()
private activity = new Subject<boolean>() private activity = new Subject<boolean>()
private destroyed = new Subject<void>() private destroyed = new Subject<void>()
@@ -84,8 +83,6 @@ export abstract class BaseTabComponent extends BaseComponent {
get focused$ (): Observable<void> { return this.focused } get focused$ (): Observable<void> { return this.focused }
get blurred$ (): Observable<void> { return this.blurred } 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 titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) }
get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) } get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) }
get activity$ (): Observable<boolean> { return this.activity } get activity$ (): Observable<boolean> { return this.activity }
@@ -180,11 +177,6 @@ export abstract class BaseTabComponent extends BaseComponent {
this.blurred.next() this.blurred.next()
} }
/* @hidden */
emitVisibility (visibility: boolean): void {
this.visibility.next(visibility)
}
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> { insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any> this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
this.viewContainer = container this.viewContainer = container

View File

@@ -18,58 +18,45 @@ export class SelectorModalComponent<T> {
@Input() selectedIndex = 0 @Input() selectedIndex = 0
hasGroups = false hasGroups = false
@ViewChildren('item') itemChildren: QueryList<ElementRef> @ViewChildren('item') itemChildren: QueryList<ElementRef>
private preventEdit: boolean
constructor (public modalInstance: NgbActiveModal) { constructor (
this.preventEdit = false public modalInstance: NgbActiveModal,
} ) { }
ngOnInit (): void { ngOnInit (): void {
this.onFilterChange() this.onFilterChange()
this.hasGroups = this.options.some(x => x.group) this.hasGroups = this.options.some(x => x.group)
} }
@HostListener('keydown', ['$event']) onKeyDown (event: KeyboardEvent): void { @HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void {
if (event.key === 'Escape') { 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') {
this.close() this.close()
} 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',
})
} }
} if (event.key === 'Backspace' && this.canEditSelected()) {
event.preventDefault()
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void { this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
if (event.key === 'Backspace' && this.preventEdit) { this.onFilterChange()
this.preventEdit = false
} }
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
} }
onFilterChange (): void { onFilterChange (): void {
@@ -89,7 +76,7 @@ export class SelectorModalComponent<T> {
{ sort: true }, { sort: true },
).search(f) ).search(f)
this.options.filter(x => x.freeInputPattern).sort(firstBy<SelectorOption<T>, number>(x => x.weight ?? 0)).forEach(freeOption => { this.options.filter(x => x.freeInputPattern).forEach(freeOption => {
if (!this.filteredOptions.includes(freeOption)) { if (!this.filteredOptions.includes(freeOption)) {
this.filteredOptions.push(freeOption) this.filteredOptions.push(freeOption)
} }

View File

@@ -275,7 +275,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
}) })
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred())) this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.visibility$.subscribe(visibility => this.getAllTabs().forEach(x => x.emitVisibility(visibility)))
this.tabAdded$.subscribe(() => this.updateTitle()) this.tabAdded$.subscribe(() => this.updateTitle())
this.tabRemoved$.subscribe(() => this.updateTitle()) this.tabRemoved$.subscribe(() => this.updateTitle())

View File

@@ -1,10 +1,10 @@
.container.mt-3.mb-3 .container.mt-5.mb-5
.mb-3 .mb-4
.tabby-logo .tabby-logo
h1.tabby-title Tabby h1.tabby-title Tabby
sup α sup α
.text-center.mb-3(translate) Thank you for downloading Tabby! .text-center.mb-5(translate) Thank you for downloading Tabby!
.form-line .form-line
.header .header
@@ -16,54 +16,13 @@
*ngFor='let lang of allLanguages' *ngFor='let lang of allLanguages'
) {{lang.name}} ) {{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 .form-line
.header .header
.title(translate) Enable analytics .title(translate) Enable analytics
.description(translate) Help track the number of Tabby installs across the world! .description(translate) Help track the number of Tabby installs across the world!
toggle([(ngModel)]='config.store.enableAnalytics') toggle([(ngModel)]='config.store.enableAnalytics')
.form-line .form-line
.header .header
.title(translate) Enable global hotkey (Ctrl-Space) .title(translate) Enable global hotkey (Ctrl-Space)

View File

@@ -6,8 +6,3 @@
max-height: 100%; max-height: 100%;
overflow-y: auto; overflow-y: auto;
} }
.tabby-logo {
width: 60px;
height: 60px;
}

View File

@@ -9,6 +9,5 @@ export class CoreConfigProvider extends ConfigProvider {
[Platform.Linux]: require('./configDefaults.linux.yaml').default, [Platform.Linux]: require('./configDefaults.linux.yaml').default,
[Platform.Web]: require('./configDefaults.web.yaml').default, [Platform.Web]: require('./configDefaults.web.yaml').default,
} }
defaults = require('./configDefaults.yaml').default defaults = require('./configDefaults.yaml').default
} }

View File

@@ -96,3 +96,5 @@ hotkeys:
- '⌘-Shift-E' - '⌘-Shift-E'
command-selector: command-selector:
- '⌘-Shift-P' - '⌘-Shift-P'
appearance:
vibrancy: true

View File

@@ -19,7 +19,6 @@ appearance:
vibrancyType: 'blur' vibrancyType: 'blur'
lastTabClosesWindow: false lastTabClosesWindow: false
spaciness: 1 spaciness: 1
colorSchemeMode: 'dark'
terminal: terminal:
showBuiltinProfiles: true showBuiltinProfiles: true
showRecentProfiles: 3 showRecentProfiles: 3
@@ -32,7 +31,6 @@ hotkeys:
profile-selectors: profile-selectors:
__nonStructural: true __nonStructural: true
profiles: [] profiles: []
groups: []
profileDefaults: profileDefaults:
__nonStructural: true __nonStructural: true
ssh: ssh:

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core' import { TranslateService } from '@ngx-translate/core'
import { ProfilesService } from './services/profiles.service' import { ProfilesService } from './services/profiles.service'
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider' import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
import { PartialProfile, Profile } from './api'
/** @hidden */ /** @hidden */
@Injectable() @Injectable()
@@ -267,7 +268,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
return [ return [
...this.hotkeys, ...this.hotkeys,
...profiles.map(profile => ({ ...profiles.map(profile => ({
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`, id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
name: this.translate.instant('New tab: {profile}', { profile: profile.name }), name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
})), })),
...this.profilesService.getProviders().map(provider => ({ ...this.profilesService.getProviders().map(provider => ({
@@ -277,4 +278,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
] ]
} }
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
return (profile.id ?? profile.name).replace(/\./g, '-')
}
} }

View File

@@ -37,7 +37,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
import { DropZoneDirective } from './directives/dropZone.directive' import { DropZoneDirective } from './directives/dropZone.directive'
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive' import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, QuickConnectProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api' import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { ConfigService } from './services/config.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.')) { if (hotkey.startsWith('profile.')) {
const id = hotkey.substring(hotkey.indexOf('.') + 1) const id = hotkey.substring(hotkey.indexOf('.') + 1)
const profiles = await profilesService.getProfiles() const profiles = await profilesService.getProfiles()
const profile = profiles.find(x => ProfilesService.getProfileHotkeyName(x) === id) const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
if (profile) { if (profile) {
profilesService.openNewTabForProfile(profile) profilesService.openNewTabForProfile(profile)
} }
@@ -188,10 +188,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (!provider) { if (!provider) {
return return
} }
this.showSelector(provider).catch(() => null) this.showSelector(provider)
} }
if (hotkey === 'command-selector') { if (hotkey === 'command-selector') {
commands.showSelector().catch(() => null) commands.showSelector()
} }
if (hotkey === 'profile-selector') { if (hotkey === 'profile-selector') {
@@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
callback: () => this.profilesService.openNewTabForProfile(p), callback: () => this.profilesService.openNewTabForProfile(p),
})) }))
if (provider instanceof QuickConnectProfileProvider) { if (provider.supportsQuickConnect) {
options.push({ options.push({
name: this.translate.instant('Quick connect'), name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'), freeInputPattern: this.translate.instant('Connect to "%s"...'),

View File

@@ -230,13 +230,11 @@ export class AppService {
if (this._activeTab) { if (this._activeTab) {
this._activeTab.clearActivity() this._activeTab.clearActivity()
this._activeTab.emitBlurred() this._activeTab.emitBlurred()
this._activeTab.emitVisibility(false)
} }
this._activeTab = tab this._activeTab = tab
this.activeTabChange.next(tab) this.activeTabChange.next(tab)
setImmediate(() => { setImmediate(() => {
this._activeTab?.emitFocused() this._activeTab?.emitFocused()
this._activeTab?.emitVisibility(true)
}) })
this.hostWindow.setTitle(this._activeTab?.title) this.hostWindow.setTitle(this._activeTab?.title)
} }

View File

@@ -101,7 +101,7 @@ export class CommandService {
context.tab = tab.getFocusedTab() ?? undefined context.tab = tab.getFocusedTab() ?? undefined
} }
const commands = await this.getCommands(context) const commands = await this.getCommands(context)
return this.selector.show( await this.selector.show(
this.translate.instant('Commands'), this.translate.instant('Commands'),
commands.map(c => ({ commands.map(c => ({
name: c.label, name: c.label,

View File

@@ -10,7 +10,6 @@ import { PlatformService } from '../api/platform'
import { HostAppService } from '../api/hostApp' import { HostAppService } from '../api/hostApp'
import { Vault, VaultService } from './vault.service' import { Vault, VaultService } from './vault.service'
import { serializeFunction } from '../utils' import { serializeFunction } from '../utils'
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
const deepmerge = require('deepmerge') const deepmerge = require('deepmerge')
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -365,47 +364,6 @@ export class ConfigService {
} }
config.version = 4 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
}
} }
private async maybeDecryptConfig (store) { private async maybeDecryptConfig (store) {

View File

@@ -13,9 +13,8 @@ export class FileProvidersService {
) { } ) { }
async selectAndStoreFile (description: string): Promise<string> { async selectAndStoreFile (description: string): Promise<string> {
return this.selectProvider().then(p => { const p = await this.selectProvider()
return p.selectAndStoreFile(description) return p.selectAndStoreFile(description)
})
} }
async retrieveFile (key: string): Promise<Buffer> { async retrieveFile (key: string): Promise<Buffer> {

View File

@@ -2,15 +2,12 @@ import { Injectable, Inject } from '@angular/core'
import { TranslateService } from '@ngx-translate/core' import { TranslateService } from '@ngx-translate/core'
import { NewTabParameters } from './tabs.service' import { NewTabParameters } from './tabs.service'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, ProfileProvider } from '../api/profileProvider' import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
import { SelectorOption } from '../api/selector' import { SelectorOption } from '../api/selector'
import { AppService } from './app.service' import { AppService } from './app.service'
import { configMerge, ConfigProxy, ConfigService } from './config.service' import { configMerge, ConfigProxy, ConfigService } from './config.service'
import { NotificationsService } from './notifications.service' import { NotificationsService } from './notifications.service'
import { SelectorService } from './selector.service' import { SelectorService } from './selector.service'
import deepClone from 'clone-deep'
import { v4 as uuidv4 } from 'uuid'
import slugify from 'slugify'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ProfilesService { export class ProfilesService {
@@ -39,127 +36,6 @@ export class ProfilesService {
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[], @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> { async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
const params = await this.newTabParametersForProfile(profile) const params = await this.newTabParametersForProfile(profile)
if (params) { if (params) {
@@ -187,40 +63,52 @@ export class ProfilesService {
return params return params
} }
async launchProfile (profile: PartialProfile<Profile>): Promise<void> { getProviders (): ProfileProvider<Profile>[] {
await this.openNewTabForProfile(profile) return [...this.profileProviders]
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)
} }
static getProfileHotkeyName (profile: PartialProfile<Profile>): string { async getProfiles (): Promise<PartialProfile<Profile>[]> {
return (profile.id ?? profile.name).replace(/\./g, '-') const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
let list = lists.reduce((a, b) => a.concat(b), [])
list = [
...this.config.store.profiles ?? [],
...list,
]
const sortKey = p => `${p.group ?? ''} / ${p.name}`
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
return list
} }
/* providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
* Methods used to interract with Profile Selector const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
*/ return provider as unknown as ProfileProvider<T>|null
}
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
profile = this.getConfigProxyForProfile(profile)
return this.providerForProfile(profile)?.getDescription(profile) ?? null
}
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> { selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
const fullProfile = this.getConfigProxyForProfile(profile) const fullProfile = this.getConfigProxyForProfile(profile)
const provider = this.providerForProfile(fullProfile) const provider = this.providerForProfile(fullProfile)
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
return { return {
...profile, ...profile,
group: this.resolveProfileGroupName(profile.group ?? ''), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
group: profile.group || '',
freeInputEquivalent, freeInputEquivalent,
description: provider?.getDescription(fullProfile), 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> { showProfileSelector (): Promise<PartialProfile<Profile>|null> {
if (this.selector.active) { if (this.selector.active) {
return Promise.resolve(null) return Promise.resolve(null)
@@ -230,12 +118,12 @@ export class ProfilesService {
try { try {
const recentProfiles = this.getRecentProfiles() const recentProfiles = this.getRecentProfiles()
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({ let options: SelectorOption<void>[] = recentProfiles.map(p => ({
...this.selectorOptionForProfile(p), ...this.selectorOptionForProfile(p),
group: this.translate.instant('Recent'), group: this.translate.instant('Recent'),
icon: 'fas fa-history', icon: 'fas fa-history',
color: p.color, color: p.color,
weight: i - (recentProfiles.length + 1), weight: -2,
callback: async () => { callback: async () => {
if (p.id) { if (p.id) {
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
@@ -289,38 +177,30 @@ export class ProfilesService {
}) })
} catch { } } catch { }
this.getProviders().forEach(provider => { this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
if (provider instanceof QuickConnectProfileProvider) { options.push({
options.push({ name: this.translate.instant('Quick connect'),
name: this.translate.instant('Quick connect'), freeInputPattern: this.translate.instant('Connect to "%s"...'),
freeInputPattern: this.translate.instant('Connect to "%s"...'), description: `(${provider.name.toUpperCase()})`,
description: `(${provider.name.toUpperCase()})`, icon: 'fas fa-arrow-right',
icon: 'fas fa-arrow-right', weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0, callback: query => {
callback: query => { const profile = provider.quickConnect(query)
const profile = provider.quickConnect(query) resolve(profile)
resolve(profile) },
}, })
})
}
}) })
await this.selector.show(this.translate.instant('Select profile or enter an address'), options).catch(() => reject()) await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
} catch (err) { } catch (err) {
reject(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> { async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
for (const provider of this.getProviders()) { for (const provider of this.getProviders()) {
if (provider instanceof QuickConnectProfileProvider) { if (provider.supportsQuickConnect) {
const profile = provider.quickConnect(query) const profile = provider.quickConnect(query)
if (profile) { if (profile) {
return profile return profile
@@ -331,178 +211,27 @@ export class ProfilesService {
return null 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 provider = this.providerForProfile(profile)
const defaults = [
return [
this.profileDefaults, this.profileDefaults,
provider?.configDefaults ?? {}, provider?.configDefaults ?? {},
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {}, !provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {}, ].reduce(configMerge, {})
] return new ConfigProxy(profile, defaults) as unknown as T
} }
/* async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
* Methods used to interract with ProfileGroup await this.openNewTabForProfile(profile)
*/
/** let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
* Synchronously return an Array of the existing ProfileGroups if (this.config.store.terminal.showRecentProfiles > 0) {
* Does not return builtin groups recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
*/ recentProfiles.unshift(profile)
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] { recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
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 })
}
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 { } else {
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) { recentProfiles = []
delete profile.group
}
} }
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
} }
/**
* 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] ?? {}
}
} }

View File

@@ -3,7 +3,7 @@ import { Subject, Observable } from 'rxjs'
import * as Color from 'color' import * as Color from 'color'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { Theme } from '../api/theme' import { Theme } from '../api/theme'
import { PlatformService, PlatformTheme } from '../api/platform' import { PlatformService } from '../api/platform'
import { NewTheme } from '../theme' import { NewTheme } from '../theme'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -194,14 +194,7 @@ export class ThemesService {
/// @hidden /// @hidden
_getActiveColorScheme (): any { _getActiveColorScheme (): any {
let theme: PlatformTheme = 'dark' if (this.platform.getTheme() === 'light') {
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 return this.config.store.terminal.lightColorScheme
} else { } else {
return this.config.store.terminal.colorScheme return this.config.store.terminal.colorScheme

View File

@@ -285,7 +285,7 @@ export class VaultFileProvider extends FileProvider {
icon: 'fas fa-file', icon: 'fas fa-file',
result: f, result: f,
})), })),
]).catch(() => null) ])
if (result) { if (result) {
return `${this.prefix}${result.key.id}` return `${this.prefix}${result.key.id}`
} }

View File

@@ -149,7 +149,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
click: async () => { click: async () => {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('Profile name') modal.componentInstance.prompt = this.translate.instant('Profile name')
const name = (await modal.result.catch(() => null))?.value const name = (await modal.result)?.value
if (!name) { if (!name) {
return return
} }
@@ -262,7 +262,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
} }
async switchTabProfile (tab: BaseTabComponent) { async switchTabProfile (tab: BaseTabComponent) {
const profile = await this.profilesService.showProfileSelector().catch(() => null) const profile = await this.profilesService.showProfileSelector()
if (!profile) { if (!profile) {
return return
} }

View File

@@ -22,6 +22,5 @@ export class ElectronConfigProvider extends ConfigProvider {
}, },
}, },
} }
defaults = {} defaults = {}
} }

View File

@@ -11,7 +11,6 @@ import { ElectronHostWindow } from './hostWindow.service'
import { ShellIntegrationService } from './shellIntegration.service' import { ShellIntegrationService } from './shellIntegration.service'
import { ElectronHostAppService } from './hostApp.service' import { ElectronHostAppService } from './hostApp.service'
import { PlatformTheme } from '../../../tabby-core/src/api/platform' import { PlatformTheme } from '../../../tabby-core/src/api/platform'
import { configPath } from '../../../app/lib/config'
const fontManager = require('fontmanager-redux') // eslint-disable-line const fontManager = require('fontmanager-redux') // eslint-disable-line
/* eslint-disable block-scoped-var */ /* eslint-disable block-scoped-var */
@@ -37,7 +36,7 @@ export class ElectronPlatformService extends PlatformService {
private translate: TranslateService, private translate: TranslateService,
) { ) {
super() super()
this.configPath = configPath this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
electron.ipcRenderer.on('host:display-metrics-changed', () => { electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next()) this.zone.run(() => this.displayMetricsChanged.next())

View File

@@ -33,7 +33,6 @@ export class ShellIntegrationService {
command: 'paste "%V"', command: 'paste "%V"',
}, },
] ]
private constructor ( private constructor (
private electron: ElectronService, private electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,

View File

@@ -70,7 +70,6 @@ export class PluginManagerService {
map(plugins => { map(plugins => {
const mapping: Record<string, PluginInfo[]> = {} const mapping: Record<string, PluginInfo[]> = {}
for (const p of plugins) { for (const p of plugins) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
mapping[p.name] ??= [] mapping[p.name] ??= []
mapping[p.name].push(p) mapping[p.name].push(p)
} }

View File

@@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
import { LogService, NotificationsService } from 'tabby-core' import { LogService, NotificationsService } from 'tabby-core'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Injector, NgZone } from '@angular/core' import { Injector, NgZone } from '@angular/core'
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal' import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
import { SerialService } from './services/serial.service' import { SerialService } from './services/serial.service'
export interface SerialProfile extends ConnectableTerminalProfile { export interface SerialProfile extends BaseTerminalProfile {
options: SerialProfileOptions options: SerialProfileOptions
} }

View File

@@ -87,11 +87,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.description(translate) Sends data one byte at a time .description(translate) Sends data one byte at a time
toggle([(ngModel)]='profile.options.slowSend') toggle([(ngModel)]='profile.options.slowSend')
li(ngbNavItem)
a(ngbNavLink, translate) Colors
ng-template(ngbNavContent)
color-scheme-selector([(model)]='profile.terminalColorScheme')
li(ngbNavItem) li(ngbNavItem)
a(ngbNavLink, translate) Login scripts a(ngbNavLink, translate) Login scripts
ng-template(ngbNavContent) ng-template(ngbNavContent)

View File

@@ -2,14 +2,14 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
import slugify from 'slugify' import slugify from 'slugify'
import deepClone from 'clone-deep' import deepClone from 'clone-deep'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NewTabParameters, SelectorService, HostAppService, Platform, TranslateService, ConnectableProfileProvider } from 'tabby-core' import { ProfileProvider, NewTabParameters, SelectorService, HostAppService, Platform, TranslateService } from 'tabby-core'
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component' import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
import { SerialTabComponent } from './components/serialTab.component' import { SerialTabComponent } from './components/serialTab.component'
import { SerialService } from './services/serial.service' import { SerialService } from './services/serial.service'
import { BAUD_RATES, SerialProfile } from './api' import { BAUD_RATES, SerialProfile } from './api'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class SerialProfilesService extends ConnectableProfileProvider<SerialProfile> { export class SerialProfilesService extends ProfileProvider<SerialProfile> {
id = 'serial' id = 'serial'
name = _('Serial') name = _('Serial')
settingsComponent = SerialProfileSettingsComponent settingsComponent = SerialProfileSettingsComponent
@@ -32,7 +32,6 @@ export class SerialProfilesService extends ConnectableProfileProvider<SerialProf
slowSend: false, slowSend: false,
input: { backspace: 'backspace' }, input: { backspace: 'backspace' },
}, },
clearServiceMessagesOnConnect: false,
} }
constructor ( constructor (

View File

@@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('Name for the new config') modal.componentInstance.prompt = this.translate.instant('Name for the new config')
modal.componentInstance.value = name modal.componentInstance.value = name
name = (await modal.result.catch(() => null))?.value name = (await modal.result)?.value
if (!name) { if (!name) {
return return
} }

View File

@@ -1,32 +0,0 @@
.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

View File

@@ -1,54 +0,0 @@
/* 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>
}

View File

@@ -1,7 +1,7 @@
.modal-header(*ngIf='defaultsMode === "disabled"') .modal-header(*ngIf='!defaultsMode')
h3.m-0 {{profile.name}} h3.m-0 {{profile.name}}
.modal-header(*ngIf='defaultsMode !== "disabled"') .modal-header(*ngIf='defaultsMode')
h3.m-0( h3.m-0(
translate='Defaults for {type}', translate='Defaults for {type}',
[translateParams]='{type: profileProvider.name}' [translateParams]='{type: profileProvider.name}'
@@ -10,7 +10,7 @@
.modal-body .modal-body
.row .row
.col-12.col-lg-4 .col-12.col-lg-4
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Name label(translate) Name
input.form-control( input.form-control(
type='text', type='text',
@@ -18,20 +18,17 @@
[(ngModel)]='profile.name', [(ngModel)]='profile.name',
) )
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Group label(translate) Group
input.form-control( input.form-control(
type='text', type='text',
alwaysVisibleTypeahead, alwaysVisibleTypeahead,
placeholder='Ungrouped', placeholder='Ungrouped',
[(ngModel)]='profileGroup', [(ngModel)]='profile.group',
[ngbTypeahead]='groupTypeahead', [ngbTypeahead]='groupTypeahead',
[inputFormatter]="groupFormatter",
[resultFormatter]="groupFormatter",
[editable]="false"
) )
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Icon label(translate) Icon
.input-group .input-group
input.form-control( input.form-control(
@@ -77,15 +74,9 @@
) )
option(ngValue='auto', translate) Auto option(ngValue='auto', translate) Auto
option(ngValue='keep', translate) Keep option(ngValue='keep', translate) Keep
option(*ngIf='isConnectable()', ngValue='reconnect', translate) Reconnect option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect
option(ngValue='close', translate) Close option(ngValue='close', translate) Close
.form-line(*ngIf='isConnectable()')
.header
.title(translate) Clear terminal after connection
toggle(
[(ngModel)]='profile.clearServiceMessagesOnConnect',
)
.mb-4 .mb-4
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent') .col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')

View File

@@ -2,7 +2,7 @@
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs' import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core' import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigProxy, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider } from 'tabby-core' import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
const iconsData = require('../../../tabby-core/src/icons.json') const iconsData = require('../../../tabby-core/src/icons.json')
const iconsClassList = Object.keys(iconsData).map( const iconsClassList = Object.keys(iconsData).map(
@@ -19,9 +19,8 @@ export class EditProfileModalComponent<P extends Profile> {
@Input() profile: P & ConfigProxy @Input() profile: P & ConfigProxy
@Input() profileProvider: ProfileProvider<P> @Input() profileProvider: ProfileProvider<P>
@Input() settingsComponent: new () => ProfileSettingsComponent<P> @Input() settingsComponent: new () => ProfileSettingsComponent<P>
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled' @Input() defaultsMode = false
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined groupNames: string[]
groups: PartialProfileGroup<ProfileGroup>[]
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
private _profile: Profile private _profile: Profile
@@ -31,14 +30,14 @@ export class EditProfileModalComponent<P extends Profile> {
private injector: Injector, private injector: Injector,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private profilesService: ProfilesService, private profilesService: ProfilesService,
config: ConfigService,
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
) { ) {
if (this.defaultsMode === 'disabled') { this.groupNames = [...new Set(
this.profilesService.getProfileGroups().then(groups => { (config.store.profiles as Profile[])
this.groups = groups .map(x => x.group)
this.profileGroup = groups.find(g => g.id === this.profile.group) .filter(x => !!x),
}) )].sort() as string[]
}
} }
colorsAutocomplete = text$ => text$.pipe( colorsAutocomplete = text$ => text$.pipe(
@@ -57,7 +56,7 @@ export class EditProfileModalComponent<P extends Profile> {
ngOnInit () { ngOnInit () {
this._profile = this.profile this._profile = this.profile
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' }) this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode)
} }
ngAfterViewInit () { ngAfterViewInit () {
@@ -73,15 +72,13 @@ export class EditProfileModalComponent<P extends Profile> {
} }
} }
groupTypeahead: OperatorFunction<string, readonly PartialProfileGroup<ProfileGroup>[]> = (text$: Observable<string>) => groupTypeahead = (text$: Observable<string>) =>
text$.pipe( text$.pipe(
debounceTime(200), debounceTime(200),
distinctUntilChanged(), distinctUntilChanged(),
map(q => this.groups.filter(g => !q || g.name.toLowerCase().includes(q.toLowerCase()))), map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))),
) )
groupFormatter = (g: PartialProfileGroup<ProfileGroup>) => g.name
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) => iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
text$.pipe( text$.pipe(
debounceTime(200), debounceTime(200),
@@ -89,12 +86,7 @@ export class EditProfileModalComponent<P extends Profile> {
) )
save () { save () {
if (!this.profileGroup) { this.profile.group ||= undefined
this.profile.group = undefined
} else {
this.profile.group = this.profileGroup.id
}
this.settingsComponentInstance?.save?.() this.settingsComponentInstance?.save?.()
this.profile.__cleanup() this.profile.__cleanup()
this.modalInstance.close(this._profile) this.modalInstance.close(this._profile)
@@ -103,9 +95,4 @@ export class EditProfileModalComponent<P extends Profile> {
cancel () { cancel () {
this.modalInstance.dismiss() this.modalInstance.dismiss()
} }
isConnectable (): boolean {
return this.profileProvider instanceof ConnectableProfileProvider
}
} }

View File

@@ -27,17 +27,9 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
i.fas.fa-fw.fa-search i.fas.fa-fw.fa-search
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter') input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3 button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
button.btn.btn-primary(ngbDropdownToggle) i.fas.fa-fw.fa-plus
i.fas.fa-fw.fa-plus span(translate) New profile
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 .list-group.mt-3.mb-3
ng-container(*ngFor='let group of profileGroups') ng-container(*ngFor='let group of profileGroups')
@@ -45,17 +37,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.list-group-item.list-group-item-action.d-flex.align-items-center( .list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='toggleGroupCollapse(group)' (click)='toggleGroupCollapse(group)'
) )
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0') .fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0') .fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}} span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
button.btn.btn-sm.btn-link.hover-reveal.ms-2( button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name', *ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); editProfileGroup(group)' (click)='$event.stopPropagation(); editGroup(group)'
) )
i.fas.fa-pencil-alt i.fas.fa-pencil-alt
button.btn.btn-sm.btn-link.hover-reveal.ms-2( button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name', *ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); deleteProfileGroup(group)' (click)='$event.stopPropagation(); deleteGroup(group)'
) )
i.fas.fa-trash-alt i.fas.fa-trash-alt
ng-container(*ngIf='!group.collapsed') ng-container(*ngIf='!group.collapsed')
@@ -75,7 +67,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.me-auto .me-auto
button.btn.btn-link.hover-reveal.ms-1(*ngIf='!profile.isTemplate', (click)='$event.stopPropagation(); launchProfile(profile)') button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); launchProfile(profile)')
i.fas.fa-play i.fas.fa-play
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto') .ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
@@ -177,12 +169,9 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.description(translate) These apply to all profiles of a given type .description(translate) These apply to all profiles of a given type
.list-group.mt-3.mb-3.content-box .list-group.mt-3.mb-3.content-box
a.list-group-item.list-group-item-action.d-flex.align-items-center( a.list-group-item.list-group-item-action(
(click)='editDefaults(provider)', (click)='editDefaults(provider)',
*ngFor='let provider of profileProviders' *ngFor='let provider of profileProviders'
) {{provider.name|translate}} ) {{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') div([ngbNavOutlet]='nav')

View File

@@ -1,27 +1,32 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' 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 deepClone from 'clone-deep'
import { Component, Inject } from '@angular/core' import { Component, Inject } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup, QuickConnectProfileProvider } from 'tabby-core' import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
import { EditProfileModalComponent } from './editProfileModal.component' import { EditProfileModalComponent } from './editProfileModal.component'
import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
interface ProfileGroup {
name?: string
profiles: PartialProfile<Profile>[]
editable: boolean
collapsed: boolean
}
_('Filter') _('Filter')
_('Ungrouped') _('Ungrouped')
interface CollapsableProfileGroup extends ProfileGroup {
collapsed: boolean
}
/** @hidden */ /** @hidden */
@Component({ @Component({
templateUrl: './profilesSettingsTab.component.pug', templateUrl: './profilesSettingsTab.component.pug',
styleUrls: ['./profilesSettingsTab.component.scss'], styleUrls: ['./profilesSettingsTab.component.scss'],
}) })
export class ProfilesSettingsTabComponent extends BaseComponent { export class ProfilesSettingsTabComponent extends BaseComponent {
profiles: PartialProfile<Profile>[] = []
builtinProfiles: PartialProfile<Profile>[] = [] builtinProfiles: PartialProfile<Profile>[] = []
templateProfiles: PartialProfile<Profile>[] = [] templateProfiles: PartialProfile<Profile>[] = []
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[] profileGroups: ProfileGroup[]
filter = '' filter = ''
Platform = Platform Platform = Platform
@@ -54,7 +59,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
async newProfile (base?: PartialProfile<Profile>): Promise<void> { async newProfile (base?: PartialProfile<Profile>): Promise<void> {
if (!base) { if (!base) {
let profiles = await this.profilesService.getProfiles() let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
profiles = profiles.filter(x => !this.isProfileBlacklisted(x)) profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0)) profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
base = await this.selector.show( base = await this.selector.show(
@@ -62,32 +67,31 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
profiles.map(p => ({ profiles.map(p => ({
icon: p.icon, icon: p.icon,
description: this.profilesService.getDescription(p) ?? undefined, description: this.profilesService.getDescription(p) ?? undefined,
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name, name: p.group ? `${p.group} / ${p.name}` : p.name,
result: p, result: p,
})), })),
).catch(() => undefined) )
if (!base) {
return
}
} }
const baseProfile: PartialProfile<Profile> = deepClone(base) const profile: PartialProfile<Profile> = deepClone(base)
delete baseProfile.id delete profile.id
if (base.isTemplate) { if (base.isTemplate) {
baseProfile.name = '' profile.name = ''
} else if (!base.isBuiltin) { } else if (!base.isBuiltin) {
baseProfile.name = this.translate.instant('{name} copy', base) profile.name = this.translate.instant('{name} copy', base)
} }
baseProfile.isBuiltin = false profile.isBuiltin = false
baseProfile.isTemplate = false profile.isTemplate = false
const result = await this.showProfileEditModal(baseProfile) const result = await this.showProfileEditModal(profile)
if (!result) { if (!result) {
return return
} }
if (!result.name) { Object.assign(profile, result)
const cfgProxy = this.profilesService.getConfigProxyForProfile(result) if (!profile.name) {
result.name = this.profilesService.providerForProfile(result)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base) const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
} }
await this.profilesService.newProfile(result) profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
this.config.store.profiles = [profile, ...this.config.store.profiles]
await this.config.save() await this.config.save()
} }
@@ -96,7 +100,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
if (!result) { if (!result) {
return return
} }
await this.profilesService.writeProfile(result) Object.assign(profile, result)
await this.config.save() await this.config.save()
} }
@@ -117,6 +121,12 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
return null 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 result.type = provider.id
return result return result
} }
@@ -134,79 +144,69 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1, cancelId: 1,
}, },
)).response === 0) { )).response === 0) {
await this.profilesService.deleteProfile(profile) this.profilesService.providerForProfile(profile)?.deleteProfile(
await this.config.save() 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)) {
async newProfileGroup (): Promise<void> { const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
const modal = this.ngbModal.open(PromptModalComponent)
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 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 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete model[k] delete profileHotkeys[profileHotkeyName]
this.config.store.hotkeys.profile = profileHotkeys
} }
Object.assign(model, result) await this.config.save()
if (!group.defaults) {
group.defaults = {}
}
group.defaults[provider.id] = model
} }
return this.showProfileGroupEditModal(group)
} }
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> { 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> {
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
await this.config.save()
}
}
async deleteGroup (group: ProfileGroup): Promise<void> {
if ((await this.platform.showMessageBox( if ((await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
@@ -219,8 +219,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1, cancelId: 1,
}, },
)).response === 0) { )).response === 0) {
let deleteProfiles = false if ((await this.platform.showMessageBox(
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
message: this.translate.instant('Delete the group\'s profiles?'), message: this.translate.instant('Delete the group\'s profiles?'),
@@ -231,26 +230,19 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
defaultId: 0, defaultId: 0,
cancelId: 0, cancelId: 0,
}, },
)).response !== 0) { )).response === 0) {
deleteProfiles = true 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)
} }
await this.profilesService.deleteProfileGroup(group, { deleteProfiles })
await this.config.save() await this.config.save()
} }
} }
async refresh (): Promise<void> { isGroupVisible (group: ProfileGroup): boolean {
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
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 { isProfileVisible (profile: PartialProfile<Profile>): boolean {
@@ -278,12 +270,11 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning' }[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
} }
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void { toggleGroupCollapse (group: ProfileGroup): void {
if (group.profiles?.length === 0) {
return
}
group.collapsed = !group.collapsed group.collapsed = !group.collapsed
this.saveProfileGroupCollapse(group) const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
profileGroupCollapsed[group.name ?? ''] = group.collapsed
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
} }
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> { async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
@@ -291,40 +282,21 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
EditProfileModalComponent, EditProfileModalComponent,
{ size: 'lg' }, { size: 'lg' },
) )
const model = this.profilesService.getProviderDefaults(provider) const model = this.config.store.profileDefaults[provider.id] ?? {}
model.type = provider.id model.type = provider.id
modal.componentInstance.profile = Object.assign({}, model) modal.componentInstance.profile = Object.assign({}, model)
modal.componentInstance.profileProvider = provider modal.componentInstance.profileProvider = provider
modal.componentInstance.defaultsMode = 'enabled' modal.componentInstance.defaultsMode = true
const result = await modal.result.catch(() => null) const result = await modal.result
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> { // Fully replace the config
if ((await this.platform.showMessageBox( for (const k in model) {
{ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
type: 'warning', delete model[k]
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 { blacklistProfile (profile: PartialProfile<Profile>): void {
@@ -342,29 +314,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
} }
getQuickConnectProviders (): ProfileProvider<Profile>[] { getQuickConnectProviders (): ProfileProvider<Profile>[] {
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider) return this.profileProviders.filter(x => x.supportsQuickConnect)
}
/**
* 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
} }
} }

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,27 +0,0 @@
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')
}
}

View File

@@ -32,9 +32,6 @@ div(*ngIf='vault.isEnabled()')
button.btn.btn-link(ngbDropdownToggle) button.btn.btn-link(ngbDropdownToggle)
i.fas.fa-ellipsis-v i.fas.fa-ellipsis-v
div(ngbDropdownMenu) div(ngbDropdownMenu)
button(ngbDropdownItem, (click)='showSecret(secret)')
i.fas.fa-fw.fa-eye
span(translate) Show
button( button(
ngbDropdownItem, ngbDropdownItem,
*ngIf='secret.type === VAULT_SECRET_TYPE_FILE', *ngIf='secret.type === VAULT_SECRET_TYPE_FILE',

View File

@@ -3,7 +3,6 @@ import { Component, HostBinding } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 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 { BaseComponent, VaultService, VaultSecret, Vault, PlatformService, ConfigService, VAULT_SECRET_TYPE_FILE, PromptModalComponent, VaultFileSecret, TranslateService } from 'tabby-core'
import { SetVaultPassphraseModalComponent } from './setVaultPassphraseModal.component' import { SetVaultPassphraseModalComponent } from './setVaultPassphraseModal.component'
import { ShowSecretModalComponent } from './showSecretModal.component'
/** @hidden */ /** @hidden */
@@ -36,11 +35,9 @@ export class VaultSettingsTabComponent extends BaseComponent {
async enableVault () { async enableVault () {
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent) const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
const newPassphrase = await modal.result.catch(() => null) const newPassphrase = await modal.result
if (newPassphrase) { await this.vault.setEnabled(true, newPassphrase)
await this.vault.setEnabled(true, newPassphrase) this.vaultContents = await this.vault.load(newPassphrase)
this.vaultContents = await this.vault.load(newPassphrase)
}
} }
async disableVault () { async disableVault () {
@@ -68,10 +65,8 @@ export class VaultSettingsTabComponent extends BaseComponent {
return return
} }
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent) const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
const newPassphrase = await modal.result.catch(() => null) const newPassphrase = await modal.result
if (newPassphrase) { this.vault.save(this.vaultContents, newPassphrase)
this.vault.save(this.vaultContents, newPassphrase)
}
} }
async toggleConfigEncrypted () { async toggleConfigEncrypted () {
@@ -98,16 +93,6 @@ export class VaultSettingsTabComponent extends BaseComponent {
return this.translate.instant('Unknown secret of type {type} for {key}', { type: secret.type, key: JSON.stringify(secret.key) }) 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) { removeSecret (secret: VaultSecret) {
if (!this.vaultContents) { if (!this.vaultContents) {
return return
@@ -133,7 +118,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
modal.componentInstance.prompt = this.translate.instant('New name') modal.componentInstance.prompt = this.translate.instant('New name')
modal.componentInstance.value = secret.key.description modal.componentInstance.value = secret.key.description
const description = (await modal.result.catch(() => null))?.value const description = (await modal.result)?.value
if (!description) { if (!description) {
return return
} }

View File

@@ -20,7 +20,6 @@ export class SettingsConfigProvider extends ConfigProvider {
}, },
}, },
} }
platformDefaults = { platformDefaults = {
[Platform.macOS]: { [Platform.macOS]: {
hotkeys: { hotkeys: {

View File

@@ -7,7 +7,6 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core' import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
import { EditProfileModalComponent } from './components/editProfileModal.component' import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component' import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component' import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
@@ -19,7 +18,6 @@ import { SetVaultPassphraseModalComponent } from './components/setVaultPassphras
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component' import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
import { ReleaseNotesComponent } from './components/releaseNotesTab.component' import { ReleaseNotesComponent } from './components/releaseNotesTab.component'
import { ConfigSyncSettingsTabComponent } from './components/configSyncSettingsTab.component' import { ConfigSyncSettingsTabComponent } from './components/configSyncSettingsTab.component'
import { ShowSecretModalComponent } from './components/showSecretModal.component'
import { ConfigSyncService } from './services/configSync.service' import { ConfigSyncService } from './services/configSync.service'
@@ -50,7 +48,6 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
], ],
declarations: [ declarations: [
EditProfileModalComponent, EditProfileModalComponent,
EditProfileGroupModalComponent,
HotkeyInputModalComponent, HotkeyInputModalComponent,
HotkeySettingsTabComponent, HotkeySettingsTabComponent,
MultiHotkeyInputComponent, MultiHotkeyInputComponent,
@@ -62,7 +59,6 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
WindowSettingsTabComponent, WindowSettingsTabComponent,
ConfigSyncSettingsTabComponent, ConfigSyncSettingsTabComponent,
ReleaseNotesComponent, ReleaseNotesComponent,
ShowSecretModalComponent,
], ],
}) })
export default class SettingsModule { export default class SettingsModule {

View File

@@ -25,7 +25,6 @@
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/ssh2": "^0.5.46", "@types/ssh2": "^0.5.46",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"diffie-hellman": "^5.0.3",
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136", "sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
"strip-ansi": "^7.0.0" "strip-ansi": "^7.0.0"
}, },

View File

@@ -1,4 +1,4 @@
import { ConnectableTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal' import { BaseTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
export enum SSHAlgorithmType { export enum SSHAlgorithmType {
HMAC = 'hmac', HMAC = 'hmac',
@@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
HOSTKEY = 'serverHostKey', HOSTKEY = 'serverHostKey',
} }
export interface SSHProfile extends ConnectableTerminalProfile { export interface SSHProfile extends BaseTerminalProfile {
options: SSHProfileOptions options: SSHProfileOptions
} }

View File

@@ -18,7 +18,6 @@ export class SFTPCreateDirectoryModalComponent extends BaseComponent {
create (): void { create (): void {
this.modalInstance.close(this.directoryName) this.modalInstance.close(this.directoryName)
} }
cancel (): void { cancel (): void {
this.modalInstance.close('') this.modalInstance.close('')
} }

View File

@@ -113,8 +113,8 @@ export class SFTPPanelComponent {
async openCreateDirectoryModal (): Promise<void> { async openCreateDirectoryModal (): Promise<void> {
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent) const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
const directoryName = await modal.result.catch(() => null) const directoryName = await modal.result
if (directoryName?.trim()) { if (directoryName !== '') {
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => { this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
this.notifications.notice('The directory was created successfully') this.notifications.notice('The directory was created successfully')
this.navigate(path.join(this.path, directoryName)) this.navigate(path.join(this.path, directoryName))

View File

@@ -160,12 +160,10 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
type='radio', type='radio',
name='auth', name='auth',
[(ngModel)]='profile.options.auth', [(ngModel)]='profile.options.auth',
id='auth"keyboardInteractive"', id='auth"keyboardInteractive"'
[value]='"keyboardInteractive"' [value]='"keyboardInteractive"'
) )
label.btn.btn-secondary( label.btn.btn-secondary(ngbButtonLabel)
for='auth"keyboardInteractive"'
)
i.far.fa-keyboard i.far.fa-keyboard
.m-0(translate) Interactive .m-0(translate) Interactive

View File

@@ -75,7 +75,7 @@ export class SSHProfileSettingsComponent {
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}` modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
modal.componentInstance.password = true modal.componentInstance.password = true
try { try {
const result = await modal.result.catch(() => null) const result = await modal.result
if (result?.value) { if (result?.value) {
this.passwordStorage.savePassword(this.profile, result.value) this.passwordStorage.savePassword(this.profile, result.value)
this.hasSavedPassword = true this.hasSavedPassword = true
@@ -89,13 +89,11 @@ export class SSHProfileSettingsComponent {
} }
async addPrivateKey () { async addPrivateKey () {
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null) const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`)
if (ref) { this.profile.options.privateKeys = [
this.profile.options.privateKeys = [ ...this.profile.options.privateKeys!,
...this.profile.options.privateKeys!, ref,
ref, ]
]
}
} }
removePrivateKey (path: string) { removePrivateKey (path: string) {

View File

@@ -61,4 +61,12 @@ h3 SSH
(ngModelChange)='config.save()' (ngModelChange)='config.save()'
) )
.form-line
.header
.title(translate) Clear terminal after connection
toggle(
[(ngModel)]='config.store.ssh.clearServiceMessagesOnConnect',
(ngModelChange)='config.save()',
)
.alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab .alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab

View File

@@ -83,7 +83,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
const jumpSession = await this.setupOneSession( const jumpSession = await this.setupOneSession(
this.injector, this.injector,
this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection), this.profilesService.getConfigProxyForProfile(jumpConnection),
) )
jumpSession.ref() jumpSession.ref()
@@ -163,6 +163,10 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
await session.start() await session.start()
if (this.config.store.ssh.clearServiceMessagesOnConnect) {
this.frontend?.clear()
}
this.session?.resize(this.size.columns, this.size.rows) this.session?.resize(this.size.columns, this.size.rows)
} }

View File

@@ -11,6 +11,7 @@ export class SSHConfigProvider extends ConfigProvider {
x11Display: null, x11Display: null,
knownHosts: [], knownHosts: [],
verifyHostKeys: true, verifyHostKeys: true,
clearServiceMessagesOnConnect: true,
}, },
hotkeys: { hotkeys: {
'restart-ssh-session': [], 'restart-ssh-session': [],

View File

@@ -1,5 +1,3 @@
import './polyfills'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'

View File

@@ -1,4 +0,0 @@
const nodeCrypto = require('crypto')
const browserDH = require('diffie-hellman/browser')
nodeCrypto.createDiffieHellmanGroup = browserDH.createDiffieHellmanGroup
nodeCrypto.createDiffieHellman = browserDH.createDiffieHellman

View File

@@ -1,5 +1,5 @@
import { Injectable, InjectFlags, Injector } from '@angular/core' import { Injectable, InjectFlags, Injector } from '@angular/core'
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core' import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants' import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component' import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
import { SSHTabComponent } from './components/sshTab.component' import { SSHTabComponent } from './components/sshTab.component'
@@ -8,9 +8,10 @@ import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
import { SSHProfileImporter } from './api/importer' import { SSHProfileImporter } from './api/importer'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> { export class SSHProfilesService extends ProfileProvider<SSHProfile> {
id = 'ssh' id = 'ssh'
name = 'SSH' name = 'SSH'
supportsQuickConnect = true
settingsComponent = SSHProfileSettingsComponent settingsComponent = SSHProfileSettingsComponent
configDefaults = { configDefaults = {
options: { options: {
@@ -44,7 +45,6 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
reuseSession: true, reuseSession: true,
input: { backspace: 'backspace' }, input: { backspace: 'backspace' },
}, },
clearServiceMessagesOnConnect: true,
} }
constructor ( constructor (

View File

@@ -34,7 +34,7 @@ export class SSHMultiplexerService {
if (!jumpConnection) { if (!jumpConnection) {
return key return key
} }
const jumpProfile = this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection) const jumpProfile = this.profilesService.getConfigProxyForProfile(jumpConnection)
key += '$' + await this.getMultiplexerKey(jumpProfile) key += '$' + await this.getMultiplexerKey(jumpProfile)
} }
return key return key

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