Compare commits

...

108 Commits

Author SHA1 Message Date
Indigo.liu 2cf6ef0036
Add support to connect in winscp throug tunnel. (#10829)
Package-Build / Lint (push) Has been cancelled Details
Docs / build (push) Has been cancelled Details
Package-Build / macOS-Build (arm64, aarch64-apple-darwin) (push) Has been cancelled Details
Package-Build / macOS-Build (x86_64, x86_64-apple-darwin) (push) Has been cancelled Details
Package-Build / Linux-Build (amd64, x64, ubuntu-24.04, x86_64-unknown-linux-gnu) (push) Has been cancelled Details
Package-Build / Linux-Build (arm64, arm64, ubuntu-24.04-arm, aarch64-unknown-linux-gnu, aarch64-linux-gnu-) (push) Has been cancelled Details
Package-Build / Linux-Build (armhf, arm, ubuntu-24.04, arm-unknown-linux-gnueabihf, arm-linux-gnueabihf-) (push) Has been cancelled Details
Package-Build / Windows-Build (arm64, aarch64-pc-windows-msvc) (push) Has been cancelled Details
Package-Build / Windows-Build (x64, x86_64-pc-windows-msvc) (push) Has been cancelled Details
CodeQL / Analyze (javascript) (push) Has been cancelled Details
2025-11-11 12:14:42 +01:00
Kairlec 7a7d3d2b77
fix: unexpected configuration was selected in the recent profiles (#10817) 2025-11-11 12:09:09 +01:00
Eugene 4f14e92e6a
bump node-pty 2025-11-11 11:54:47 +01:00
Eugene 52463555ab
Disable SSH compression by default as a workaround for SFTP upload bugs 2025-11-11 11:35:01 +01:00
osm1892 bbcb026433
fix: adjust ssh config time resolution (#10803)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 09:47:04 +02:00
Eugene 34c0c780f4
fixed signing 2025-10-16 01:50:41 +02:00
Eugene de65a724f3
bump russh 2025-10-13 23:18:40 +02:00
Florian Kefferpütz 264f1cbe85
Pr askeverytime fix https://github.com/Eugeny/tabby/issues/10681 (#10786) 2025-10-13 21:34:31 +02:00
allcontributors[bot] 6da2259dc3
add et304383 as a contributor for code (#10802)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-13 21:16:05 +02:00
Eugene c4db2863fa
bump russh, enable mlkem768x25519-sha256 - fixes #10792, fixes #10560 2025-10-13 21:15:10 +02:00
et304383 cf5b99403e
Bump electron to version 38 (#10801) 2025-10-13 21:04:43 +02:00
Eugene e03ded3fdb
bump russh 2025-10-10 01:03:48 +02:00
zhaolei d92540f08a
fixed #10726 (#10733) 2025-10-04 11:33:18 +02:00
Seungyong Hong e76314db6f
GH-10749: Widen dragging area when left tab and thin. (#10750) 2025-10-04 11:32:53 +02:00
Eugene 5060344c69
bump russh for pageant fixes 2025-09-24 23:20:36 +02:00
Eugene d1196a0e2a
Revert "disable SSH keepalive by default"
This reverts commit b31c2a5c11.
2025-09-24 23:14:38 +02:00
Eugene 61f02d1f85
fixed #10619 2025-08-25 20:32:17 +02:00
Eugene 0e09945629
bump electron - fixes #10633, fixes #10609, fixes #10666 2025-08-24 11:25:32 +02:00
Eugene 79129e61c7
bumped russh 2025-08-22 22:34:02 +02:00
Eugene 9e44ba670d
bump russh 2025-08-20 22:00:35 +02:00
Eugeny ea7cb1f094 Merge branch 'master' of https://github.com/Eugeny/tabby 2025-08-17 22:14:45 +02:00
Eugeny a2d27fe6a1 fixed node-pty conpty assertion failure - fixes #10618 2025-08-17 22:14:36 +02:00
Eugene ae3c57fc54
updated contributors 2025-08-15 10:39:54 +02:00
Eugeny b9e335817d bump node-pty 2025-08-09 16:17:25 +02:00
Eugene 56ee368b63
SFTP filter bar (#10590) 2025-07-25 20:20:59 +02:00
Eugene b31c2a5c11
disable SSH keepalive by default 2025-07-23 09:31:31 +02:00
Eugene 3c17654180
fixed zmodem corruption issues (#10587) 2025-07-23 09:31:01 +02:00
Eugene 7e1905c32c
SFTP folder downloads (#10586) 2025-07-23 08:38:25 +02:00
shchae04 bbf3b785fc
Fix typos (#10582) 2025-07-21 14:02:01 +02:00
SilverFox 0cf9886270
fixes #10039 - ignore focus escape sequences when auto-scrolling to the bottom (#10555) 2025-06-27 10:27:09 +02:00
Eugene 93e43067de
fixed #9953 - use ng-bootstrap tooltips 2025-06-16 23:23:37 +02:00
Eugene d1293c6a89
update author field 2025-06-16 23:13:18 +02:00
Eugene 5a7a06e529
fixed #10034 - added auto-sudo-password plugin 2025-06-16 23:12:36 +02:00
Eugene c8aea9d8e0
log exact errors when plugin install fails 2025-06-16 22:29:27 +02:00
Eugene 57001d4dde
blacklist fig plugins 2025-06-16 22:28:48 +02:00
Eugene ef59394b79
Merge branch 'master' of github.com:Eugeny/tabby 2025-05-25 11:47:18 +02:00
loopx9 020372c902
add ssh compression option (#10396) 2025-05-25 08:48:09 +02:00
Eugene 912e0aa426
fixed #10454 - "Warn on multi-line paste" fails to trigger on Unix-style newlines (\n) in Windows 2025-05-25 08:46:34 +02:00
Eugene 4a5087afc1
fixed #5672 - incorrect microphone entitlement on macOS 2025-05-25 08:43:54 +02:00
Eugene e0c34ef7bc
fixed #10482 - duplicate browser tab opening when clicking a link with an IP in it 2025-05-24 15:19:43 +02:00
Eugene 406e9e1c42
bump electron to v36 (#10506) 2025-05-24 15:09:47 +02:00
Clem 7ac85a329e
fix(ssh/settings): formats date value according to locale rules (#10468) 2025-05-15 21:51:05 +02:00
Roman af5d3b729b
chore: update GitHub actions (#10480) 2025-05-11 15:58:01 +02:00
thuanpham582002 c3dfb351fc
Add new plugins to README files for multiple languages (#10440)
Co-authored-by: thuanpt <thuanpt@mpt.com.vn>
2025-04-15 23:48:10 +02:00
Eugene 7b942c4e28
fix search hotkey (#10371) 2025-03-28 15:29:24 +01:00
gh-log c5759b29ca
search_next_previous_keyboard (#10371)
Co-authored-by: gh-log <>
2025-03-27 23:15:27 +01:00
gh-log 66257c87af
Packagecloud_upload_for_Debian_testing_unstable (#10370)
Co-authored-by: gh-log <>
2025-03-27 23:08:38 +01:00
gh-log 424b062d5b
scroll_terminal_by_line (#10353)
Co-authored-by: gh-log <>
2025-03-12 00:48:34 +01:00
Eugene 5deb725758
bumped russh again 2025-03-10 07:50:20 +01:00
Eugene 4069c22891
bumped russh for EXT_INFO race condition fix 2025-03-09 19:27:48 +01:00
mannjani 4a515d9432
Add support for %h and %r escape characters in IdentityFile for SSH. (#10343)
* Add support for %h and %r escape characters in IdentityFile for SSH.
See section "IdentityFile" on https://linux.die.net/man/5/ssh_config for
details.

* Fix lint warnings.
2025-03-05 09:28:31 +01:00
Eugene b83b2e5acc
Update README.ru-RU.md 2025-02-28 10:11:30 +01:00
Eugene e407ee8bf1
fixed #9845, fixed #10302 - allow alternative OSC52 suffix 2025-02-25 00:40:28 +01:00
Eugene c7b39bdca7
bump russh for 1024 bit rsa key support 2025-02-25 00:37:24 +01:00
ianaflous 934cdff0f8
Using ssh default profile(user/password/port) without host (#10076) 2025-02-25 00:14:27 +01:00
Timo Schnaible ab87099b8b
add debian bullseye, bookworm and trixie deb package upload (#10314) 2025-02-25 00:12:45 +01:00
Eugene 47b4b54557
bump russh for agent RSA auth fixes 2025-02-21 10:48:51 +01:00
OpaqueGlass 15f4182e0e
Fix: Unable to launch WinSCP for SSH sessions using private key (#10308) 2025-02-19 10:27:59 +01:00
aminelch 4be1e12559
Add Tokyonight color scheme (#10283) 2025-02-03 10:06:20 +01:00
Eugene 5d2d179677
prefer saved password to keyboard interactive auth 2025-01-29 10:37:28 +01:00
Eugene 4197cefdfd
bump russh for events fix 2025-01-28 10:57:56 +01:00
Eugene 7c1421ffcf
bump russh for async trait 2025-01-28 09:00:50 +01:00
Eugene 380c306d89
added warning when server disconnects during auth 2025-01-27 14:53:44 +01:00
Eugene cf0da75224
bump russh for best hash selection in agent auth 2025-01-27 14:53:32 +01:00
Eugene d1c1b48502
bump russh for rsa hash autoselection 2025-01-25 12:56:54 +01:00
Eugene a3c5b41bb1
electron builder fixes 2025-01-22 23:44:36 +01:00
Eugene 2fa7678bec
Squashed commit of the following:
commit 4efcf1bbd6
Author: Eugene <inbox@null.page>
Date:   Wed Jan 22 23:07:24 2025 +0100

    Update build.yml

commit abea964d94
Author: Eugene <inbox@null.page>
Date:   Wed Jan 22 22:59:28 2025 +0100

    Update build.yml

commit 2e7b66ac60
Author: Eugene <inbox@null.page>
Date:   Wed Jan 22 22:40:11 2025 +0100

    native arm64 build
2025-01-22 23:28:47 +01:00
Eugene c6939b114d
updated locales 2025-01-22 23:07:08 +01:00
Eugene d3e8e2a6af
bump electron-builder 2025-01-22 23:01:57 +01:00
Eugene d1b161364b
bump russh for error handling crash fix - fixes #10184, fixes #10244, fixes #10178, fixes #8481, fixes #10215, fixes #10203, fixes #7659 2025-01-22 22:44:47 +01:00
Eugene c4a514fc4a
fixed agent login regression - fixes #10248, fixes #10174 2025-01-22 22:33:26 +01:00
Eugene d525374061
fixed missing app theme at the initial vault password prompt 2025-01-22 22:16:17 +01:00
Eugene 6db08b765f
lint 2025-01-16 22:30:36 +01:00
Eugene d8d346c507
bump russh for agent fix 2025-01-16 22:27:11 +01:00
Eugene 6ffeb61c9c
fixed dropping files into the terminal not inserting the path - fixes #10221, fixes #10206 2025-01-16 22:24:24 +01:00
Eugene 92c729dada
bump russh for keyboard-interactive fixes and lock race fix 2025-01-16 22:14:29 +01:00
PytatoDuck 302f88058c
Added Sponsors Logos in README.md (#10200) 2025-01-13 20:46:29 +01:00
fireblue 66c173b1b5
Keep the translucency effect even when the window loses focus on macOS. (#10196) 2025-01-13 19:06:09 +01:00
fireblue f9dadf0816
Set the application's dark mode to follow the app settings on macOS. (#10186) 2025-01-10 22:07:17 +01:00
Eugene 0a475daa9c
fixed #10180 - encrypted PPK keys 2025-01-09 21:06:01 +01:00
Eugene ba7c31d940
fixed #10146 - agent auth 2025-01-09 21:05:30 +01:00
Eugene 52a85e4f36
lint 2025-01-08 18:44:00 +01:00
Eugene ddbb2feb9c
bump russh for jump host fix - fixes #10166 2025-01-08 18:37:56 +01:00
Eugene c43ee28a0c
Update install-deps.mjs 2025-01-08 16:44:06 +01:00
Eugene 9f3c7a95cf
Update install-deps.mjs 2025-01-08 16:08:51 +01:00
Eugene 02205410b6
Update build.yml 2025-01-08 14:31:57 +01:00
Eugene 9bcbde265d
Update build-macos.mjs 2025-01-08 12:10:06 +01:00
Eugene fee857a0f6
ci: bump node 2025-01-08 11:57:04 +01:00
Eugene 73f978781c
bump electron builder 2025-01-08 11:53:20 +01:00
Eugene 87e99654f6
bump russh for EC key support 2025-01-08 11:20:33 +01:00
Eugene 93513541f7
tweak electron fuses 2025-01-08 09:59:56 +01:00
Eugene 60a6c148d8
bump russh for freeze fix 2025-01-07 22:11:34 +01:00
Eugene 070343aebe
bump russh for ppk2 and remainingMethods support 2025-01-06 22:19:29 +01:00
Eugeny fe376a48d8 Update node-pty+1.0.0.patch 2025-01-06 15:31:30 +01:00
Eugeny 2008f30cf5 downgrade node-pty - fixes #10134 2025-01-06 14:13:39 +01:00
Eugene 53ca484ca6
Update webpack.config.mjs 2025-01-06 12:13:58 +01:00
Eugene fe46ed7847
Update default_settings 2025-01-06 11:47:52 +01:00
Eugene e90731f427
bumpe node-pty 2025-01-06 11:22:17 +01:00
Eugene 025cc68ad8
bump windows-process-tree 2025-01-06 10:51:23 +01:00
Eugene 6edb1e9cd4
bump blurbehind 2025-01-06 10:03:17 +01:00
Eugene c6981a8216
bumped russh 2025-01-06 02:52:23 +01:00
Eugene 5d318478ec
updated contributors 2025-01-02 16:58:20 +01:00
Hiroaki Ogasawara 39e3ba35c4
Update sshImporters.ts to parse Include directive (#10105) 2025-01-01 01:13:02 +01:00
Hiroaki Ogasawara d0dd09ad88
fix: case-insensitive hostname check (#10130) 2025-01-01 01:08:20 +01:00
geodic 1e44d8c525
Bump Electron to 32 as 29 is EOL (#10118)
Co-authored-by: Eugene <inbox@null.page>
2024-12-31 17:26:02 +01:00
Eugene a10e9c05b6
bumped russh for ppk support 2024-12-30 23:53:31 +01:00
Eugene 6b0b1563c5
bump russh 2024-12-28 14:12:55 +01:00
Eugene 04b34b0edd
bumped russh to 0.0.4 - ref #10117 2024-12-26 18:42:36 +01:00
110 changed files with 2024 additions and 720 deletions

View File

@ -1337,6 +1337,42 @@
"contributions": [
"financial"
]
},
{
"login": "xhiroga",
"name": "Hiroaki Ogasawara",
"avatar_url": "https://avatars.githubusercontent.com/u/13391129?v=4",
"profile": "http://hiroga.hatenablog.com/",
"contributions": [
"code"
]
},
{
"login": "geodic",
"name": "geodic",
"avatar_url": "https://avatars.githubusercontent.com/u/64704703?v=4",
"profile": "https://github.com/geodic",
"contributions": [
"code"
]
},
{
"login": "pfoundation",
"name": "P Foundation",
"avatar_url": "https://avatars.githubusercontent.com/u/80860929?v=4",
"profile": "https://p.foundation/",
"contributions": [
"financial"
]
},
{
"login": "et304383",
"name": "et304383",
"avatar_url": "https://avatars.githubusercontent.com/u/2693414?v=4",
"profile": "https://github.com/et304383",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -2,21 +2,22 @@ name: Package-Build
on: [push, pull_request]
jobs:
Lint:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.4.0
with:
node-version: 18
node-version: 22
- name: Install deps
run: |
sudo apt-get update && sudo apt-get install -y libfontconfig1-dev
npm i -g yarn
cd app
yarn
@ -48,14 +49,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.4.0
with:
node-version: 18
node-version: 22
- run: rustup target add ${{matrix.rust_triple}}
@ -129,7 +130,7 @@ jobs:
path: artifact-zip
Linux-Build:
runs-on: ubuntu-20.04
runs-on: ${{matrix.os}}
needs: Lint
strategy:
matrix:
@ -137,14 +138,17 @@ jobs:
- build-arch: x64
arch: amd64
rust_triple: x86_64-unknown-linux-gnu
os: ubuntu-24.04
- build-arch: arm64
arch: arm64
rust_triple: aarch64-unknown-linux-gnu
triplet: aarch64-linux-gnu-
os: ubuntu-24.04-arm
- build-arch: arm
arch: armhf
rust_triple: arm-unknown-linux-gnueabihf
triplet: arm-linux-gnueabihf-
os: ubuntu-24.04
fail-fast: false
env:
@ -157,30 +161,34 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.4.0
with:
node-version: 18
node-version: 22
- name: Install FPM
run: |
sudo gem install fpm
- run: rustup target add ${{matrix.rust_triple}}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libarchive-tools zsh crossbuild-essential-${{matrix.arch}}
sudo apt-get install libfontconfig1-dev libarchive-tools zsh crossbuild-essential-${{matrix.arch}}
- name: Setup tar to run as root
run: sudo chmod u+s "$(command -v tar)"
if: matrix.build-arch != 'x64'
if: matrix.build-arch == 'arm'
- name: Download cached sysroot
uses: actions/cache@v3
id: dl-cached-sysroot
if: matrix.build-arch !='x64'
if: matrix.build-arch == 'arm'
with:
key: sysroot-${{matrix.build-arch}}
path: /${{matrix.build-arch}}-sysroot
@ -190,7 +198,7 @@ jobs:
sudo apt-get update -y && sudo apt-get install debootstrap qemu-user-static binfmt-support -y
sudo qemu-debootstrap --include=libfontconfig1-dev,libsecret-1-dev,libnss3,libatk1.0-0,libatk-bridge2.0-0,libgdk-pixbuf2.0-0,libgtk-3-0,libgbm1 --variant=buildd --exclude=snapd --components=main,restricted,universe,multiverse --extractor=dpkg-deb --arch ${{matrix.arch}} bionic /${{matrix.build-arch}}-sysroot/ http://ports.ubuntu.com/ubuntu-ports/
sudo find /${{matrix.build-arch}}-sysroot -type l -lname '/*' -exec sh -c 'file="$0"; dir=$(dirname "$file"); target=$(readlink "$0"); prefix=$(dirname "$dir" | sed 's@[^/]*@\.\.@g'); newtarget="$prefix$target"; ln -snf $newtarget $file' {} \; ;
if: matrix.build-arch != 'x64' && steps.dl-cached-sysroot.outputs.cache-hit != 'true'
if: matrix.build-arch == 'arm' && steps.dl-cached-sysroot.outputs.cache-hit != 'true'
- name: Setup env to use ${{matrix.build-arch}} sysroot
run: |
@ -205,9 +213,9 @@ jobs:
elif [[ ${{matrix.arch}} == 'arm64' ]]; then
echo "PKG_CONFIG_PATH=/${{matrix.build-arch}}-sysroot/usr/lib/pkgconfig/:/${{matrix.build-arch}}-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig/" >> $GITHUB_ENV
fi
if: matrix.build-arch != 'x64'
if: matrix.build-arch == 'arm'
- name: Install npm_modules (amd64)
- name: Install npm_modules (native)
run: |
npm i -g yarn node-gyp
yarn --network-timeout 1000000 --arch=${{matrix.build-arch}} --target-arch=${{matrix.build-arch}}
@ -224,6 +232,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KEYGEN_TOKEN: ${{ secrets.KEYGEN_TOKEN }}
USE_HARD_LINKS: false
USE_SYSTEM_FPM: true
# DEBUG: electron-builder,electron-builder:*
- name: Build web resources (amd64 only)
@ -249,7 +258,7 @@ jobs:
repo: 'eugeny/tabby'
dir: 'dist'
rpmvers: 'el/9 el/8 ol/6 ol/7'
debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble ubuntu/oracular debian/jessie debian/stretch debian/buster'
debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble ubuntu/oracular debian/jessie debian/stretch debian/buster debian/bullseye debian/bookworm debian/trixie debian/testing debian/unstable'
- uses: actions/upload-artifact@master
name: Upload AppImage (${{matrix.arch}})
@ -306,18 +315,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Code signing with Software Trust Manager
uses: digicert/ssm-code-signing@v1.0.0
uses: digicert/ssm-code-signing@v1.1.1
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags'))
env:
FORCE_DOWNLOAD_TOOLS: 'true'
- name: Installing Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.4.0
with:
node-version: 18
node-version: 22
- run: npm i -g npx
- run: rustup target add ${{matrix.rust_triple}}
@ -363,6 +374,7 @@ jobs:
# not used but necessary for electron-builder to run
$env:WIN_CSC_LINK=$env:SM_CLIENT_CERT_FILE
$env:WIN_CSC_KEY_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD
node scripts/build-windows.mjs
env:
ARCH: ${{matrix.arch}}
@ -370,7 +382,7 @@ jobs:
KEYGEN_TOKEN: ${{ secrets.KEYGEN_TOKEN }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_PUBLISHER_NAME: ${{ secrets.SM_PUBLISHER_NAME }}
SM_API_KEY: ${{ vars.SM_API_KEY }}
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_HOST: ${{ vars.SM_HOST }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ vars.SM_CODE_SIGNING_CERT_SHA1_HASH }}
SM_KEYPAIR_ALIAS: ${{ vars.SM_KEYPAIR_ALIAS }}

View File

@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -7,14 +7,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v4.4.0
with:
node-version: 20
node-version: 22
- name: Build
run: |

View File

@ -60,7 +60,7 @@ tabby
| ├─ src # Electron renderer code
| └─ main.js # Electron main entry point
├─ build
├─ clink # Clink distributive, for Windows
├─ clink # Clink distribution, for Windows
├─ scripts # Maintenance scripts
├─ tabby-community-color-schemes # Plugin that provides color schemes
├─ tabby-core # Plugin that provides base UI and tab management

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2017 Eugene Pankov
Copyright (c) 2017 Tabby Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -119,7 +119,9 @@ Plugins und Themen können direkt aus der Ansicht "Einstellungen" in Tabby insta
* [clippy](https://github.com/Eugeny/tabby-clippy) - ein Beispiel-Plugin, das einen die ganze Zeit nervt
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - ermöglicht das Erstellen eigener Workspace-Profile auf Basis der angegebenen Konfiguration
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - öffnet den Standard-Systembrowser mit einem Text, der aus dem Tabby Tab ausgewählt wurde
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - Öffnet ein SFTP-Tab für SSH-Verbindungen ähnlich wie SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - In-App-Web-Authentifizierungspopups (Hauptsächlich für die in-Browser-Authentifizierung von Warpgate entwickelt)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Leistungsstarke Model Context Protocol Server-Integration für Tabby, die sich nahtlos mit KI-Assistenten über MCP-Clients wie Cursor und Windsurf verbindet und Ihren Terminal-Workflow mit intelligenten KI-Funktionen verbessert.
<a name="themes"></a>
@ -343,6 +345,12 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -120,7 +120,9 @@ Los plugins y los temas se pueden instalar directamente desde la vista de Config
* [clippy](https://github.com/Eugeny/tabby-clippy) - un ejemplo de plugin que te molesta todo el tiempo
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - permite crear perfiles de espacio de trabajo personalizados basados en la configuración dada
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - abre el navegador del sistema por defecto con un texto seleccionado en la pestaña de Tabby's
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - abre una pestaña sftp para la conexión ssh como SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - Ventanas emergentes de autenticación web dentro de la app (Construidas principalmente para la autenticación en el navegador de warpgate)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Potente integración del servidor de Protocolo de Contexto de Modelo para Tabby que se conecta sin problemas con asistentes de IA a través de clientes MCP como Cursor y Windsurf, mejorando tu flujo de trabajo en terminal con capacidades inteligentes de IA.
<a name="themes"></a>
# Temas
@ -345,6 +347,12 @@ Gracias a estas maravillosas personas ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -120,8 +120,9 @@ Tema dan Plugin bisa langsung di install dari Pengaturan di dalam Tabby.
* [clippy](https://github.com/Eugeny/tabby-clippy) - suatu contoh plugin yang akan mengganggu anda setiap saat
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - memperbolehkan membuat kustom profil workspace dari konfigurasi yang diberikan
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - membuka browser default dengan text yang dipilih dari Tab Tabby
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - membuka tab sftp untuk koneksi ssh seperti SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - Pop-up otentikasi web di dalam aplikasi (Dibangun terutama untuk otentikasi in-browser warpgate)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Integrasi server Model Context Protocol yang kuat untuk Tabby yang terhubung secara mulus dengan asisten AI melalui klien MCP seperti Cursor dan Windsurf, meningkatkan alur kerja terminal Anda dengan kemampuan AI yang cerdas.
<a name="themes"></a>
# Tema
@ -342,6 +343,12 @@ Terima kasih kepada mereka yang telah membantu ([emoji key](https://allcontribut
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -107,18 +107,19 @@ Tabby può essere eseguito come app portatile su Windows, se crei una cartella `
<a name="plugins"></a>
# Plugin
Plugins and themes can be installed directly from the Settings view inside Tabby.
* [docker](https://github.com/Eugeny/tabby-docker) - connect to Docker containers
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
* [save-output](https://github.com/Eugeny/tabby-save-output) - record terminal output into a file
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
* [clippy](https://github.com/Eugeny/tabby-clippy) - an example plugin which annoys you all the time
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - allows creating custom workspace profiles based on the given config
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - opens default system browser with a text selected from the Tabby's tab
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
I plugin e i temi possono essere installati direttamente dalla vista Impostazioni all'interno di Tabby.
* [docker](https://github.com/Eugeny/tabby-docker) - connessione ai container Docker
* [title-control](https://github.com/kbjr/terminus-title-control) - consente di modificare il titolo delle schede del terminale fornendo un prefisso, un suffisso e/o stringhe da rimuovere
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - invia rapidamente comandi a una o tutte le schede del terminale
* [save-output](https://github.com/Eugeny/tabby-save-output) - registra l'output del terminale in un file
* [sync-config](https://github.com/starxg/terminus-sync-config) - sincronizza la configurazione con Gist o Gitee
* [clippy](https://github.com/Eugeny/tabby-clippy) - un esempio di plugin che ti infastidisce tutto il tempo
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - consente di creare profili di spazio di lavoro personalizzati basati sulla configurazione fornita
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - apre il browser di sistema predefinito con un testo selezionato dalla scheda di Tabby
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - apre una scheda SFTP per la connessione SSH come SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - Popup di autenticazione web in-app (costruito principalmente per l'autenticazione in-browser di Warpgate)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Potente integrazione del server Model Context Protocol per Tabby che si connette perfettamente con assistenti AI attraverso client MCP come Cursor e Windsurf, migliorando il tuo flusso di lavoro del terminale con capacità AI intelligenti.
<a name="themes"></a>
# Temi
@ -338,6 +339,12 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -127,8 +127,9 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
* [clippy](https://github.com/Eugeny/tabby-clippy) - プラグインの作例として、いつも厄介なあいつが出てくるプラグイン
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 指定された設定からカスタマイズされたワークスペースを作成することができます
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby内の端末で選択したテキストを既定ブラウザで開くことができます。
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - SecureCRTのようにSSH接続のSFTPタブを開く
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - アプリ内ウェブ認証ポップアップ主にwarpgateのブラウザ認証用に構築
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - TabbyのためのパワフルなModel Context Protocolサーバー統合機能で、CursorやWindsurfなどのMCPクライアントを通じてAIアシスタントとシームレスに接続し、インテリジェントなAI機能によってターミナルワークフローを強化します。
<a name="themes"></a>
# テーマ
@ -353,6 +354,12 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -107,7 +107,7 @@ This README is also available in: <a href="./README.md">:gb: English</a> · <a
플러그인과 테마는 Tabby 내부의 설정에서 직접 설치할 수 있습니다.
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - m터미널의 경로 및 URL을 클릭 가능하게
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - 터미널의 경로 및 URL을 클릭 가능하게
* [docker](https://github.com/Eugeny/tabby-docker) - Docker 컨테이너에 연결
* [title-control](https://github.com/kbjr/terminus-title-control) - 접두사, 접미사 및/또는 문자열 제거를 제공하여 터미널 탭의 제목을 수정
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 하나 또는 모든 터미널 탭에 신속한 명령 전송
@ -116,8 +116,9 @@ This README is also available in: <a href="./README.md">:gb: English</a> · <a
* [clippy](https://github.com/Eugeny/tabby-clippy) - 항상 당신을 귀찮게 하는 예제 플러그인
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 주어진 구성을 기반으로 사용자 정의 작업 공간 프로필을 생성할 수 있습니다
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - Tabby의 탭에서 선택한 텍스트로 기본 시스템 브라우저를 엽니다
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - SecureCRT와 유사하게 SSH 연결에 대한 SFTP 탭을 엽니다.
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - 앱 내 웹 인증 팝업 (주로 warpgate 브라우저 인증을 위해 구축)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Cursor 및 Windsurf와 같은 MCP 클라이언트를 통해 AI 어시스턴트와 원활하게 연결되는 Tabby용 강력한 모델 컨텍스트 프로토콜 서버 통합으로, 지능형 AI 기능으로 터미널 워크플로우를 향상시킵니다.
<a name="themes"></a>
# 테마
@ -143,7 +144,7 @@ Pull requests and plugins are welcome!
---
<a name="contributors"></a>
여기있는 멋진 사람들에게 진심으로 감사합니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
여기 있는 멋진 사람들에게 진심으로 감사합니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
@ -337,6 +338,12 @@ Pull requests and plugins are welcome!
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -129,6 +129,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
* [background](https://github.com/moemoechu/tabby-background) - change Tabby background image and more...
* [highlight](https://github.com/moemoechu/tabby-highlight) - Tabby terminal keyword highlight plugin
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - In-app web authentication popups (Built primarily for warpgate in-browser auth)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Powerful Model Context Protocol server integration for Tabby that seamlessly connects with AI assistants through MCP clients like Cursor and Windsurf, enhancing your terminal workflow with intelligent AI capabilities.
<a name="themes"></a>
@ -144,7 +145,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
# Sponsors <!-- omit in toc -->
[![](https://assets-production.packagecloud.io/assets/packagecloud-logo-light-scaled-26ce8e96060fddf74afbd4445e63ba35590d4aaa56edc98495bb390ef3cae0ae.png)](https://packagecloud.io)
<a href="https://packagecloud.io"><img src="https://assets-production.packagecloud.io/assets/logo_v1-d5895e7b89b2dee19030e85515fd0f91d8f3b37c82d218a6531fc89c2b1b613c.png" width="200"></a>
[**packagecloud**](https://packagecloud.io) has provided free Debian/RPM repository hosting
@ -152,7 +153,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
[**keygen**](https://keygen.sh/?via=eugene) has provided free release & auto-update hosting
<a href="https://iqhive.com/"><img src="https://private-user-images.githubusercontent.com/161476/361584584-ed292436-1d50-46bc-b479-78222c83ed22.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ3MDg3NjgsIm5iZiI6MTcyNDcwODQ2OCwicGF0aCI6Ii8xNjE0NzYvMzYxNTg0NTg0LWVkMjkyNDM2LTFkNTAtNDZiYy1iNDc5LTc4MjIyYzgzZWQyMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODI2JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyNlQyMTQxMDhaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1iYzNlZjIxN2JiYzBkYTU5NGE4YmZlZDJiNmIxZWE1ZTAyOTNhYjJlZTRhOGZjYTk4N2E4MzMzZjg0ZTNkZWQ0JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.pQzR2d71YV4TIxOH3Lg20HpNKrm_r2D-xfkEM_F2DTs" width="100"></a>
<a href="https://iqhive.com/"><img src="https://iqhive.com/img/icons/logo.svg" width="200"></a>
[**IQ Hive**](https://iqhive.com) is providing financial support for the project development
@ -361,6 +362,12 @@ 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/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -132,6 +132,7 @@ Wtyczki (jak i motywy) mogą być instalowane bezpośrednio z widoku ustawień w
* [background](https://github.com/moemoechu/tabby-background) - zmień tło Tabby oraz wiele więcej
* [highlight](https://github.com/moemoechu/tabby-highlight) - Tabby terminal keyword highlight plugin
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - okienka autoryzacji wewnątrz konsoli (Zbudowany głównie pod autoryzację projektu Warpgate w przeglądarce)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Potężna integracja serwera Model Context Protocol dla Tabby, która bezproblemowo łączy się z asystentami AI poprzez klientów MCP, takich jak Cursor i Windsurf, usprawniając przepływ pracy w terminalu dzięki inteligentnym możliwościom AI.
<a name="themes"></a>

View File

@ -120,8 +120,9 @@ Plugins e temas podem ser instalados durante a execução na pagina de configura
* [clippy](https://github.com/Eugeny/tabby-clippy) - um plugin de exemplo que te incomoda o tempo todo
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - permite criar perfis de espaço de trabalho personalizados com base na configuração fornecida
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - abre o navegador padrão do sistema com um texto selecionado na guia do Tabby
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - open sftp tab for ssh connection like SecureCRT
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - abre uma guia SFTP para conexão SSH como o SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - pop-ups de autenticação web dentro do aplicativo (Construído principalmente para autenticação in-browser do warpgate)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Integração poderosa do servidor Model Context Protocol para Tabby que se conecta perfeitamente com assistentes de IA através de clientes MCP como Cursor e Windsurf, aprimorando seu fluxo de trabalho no terminal com recursos inteligentes de IA.
<a name="themes"></a>
# Temas
@ -346,6 +347,12 @@ Obrigado vai para essas pessoas maravilhosas ([emoji key](https://allcontributor
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -31,7 +31,7 @@
* Встроенный SSH- и Telnet-клиент и менеджер подключений;
* Встроенный последовательный терминал;
* Темы и цветовые схемы;
* Полностью настраеваемые сочетания клавиш;
* Полностью настраиваемые сочетания клавиш;
* Панели;
* Запоминание вкладок;
* Поддержка PowerShell (and PS Core), WSL, Git-Bash, Cygwin, MSYS2, Cmder и CMD;
@ -118,7 +118,8 @@
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) — позволяет создавать пользовательские профили рабочего окружения на основе конфига;
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) — открывает браузер по умолчанию с текстом, выделенном во вкладке Tabby.
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - открывает sftp вкладку для ssh соединения, похож на SecureCRT
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - Встроенные всплывающие окна веб-аутентификации (Основано в основном для аутентификации в браузере warpgate)
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - Мощная интеграция сервера протокола Model Context Protocol для Tabby, которая беспрепятственно соединяется с ИИ-ассистентами через MCP-клиенты, такие как Cursor и Windsurf, улучшая рабочий процесс в терминале с помощью интеллектуальных возможностей искусственного интеллекта.
<a name="themes"></a>
# Темы
@ -338,6 +339,12 @@ Pull-запросы и плагины приветствуются!
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -117,7 +117,8 @@
* [workspace-manager](https://github.com/composer404/tabby-workspace-manager) - 允许根据给定的配置创建自定义工作区配置文件
* [search-in-browser](https://github.com/composer404/tabby-search-in-browser) - 从 Tabby 标签页带有选中的文本来打开系统默认浏览器
* [sftp-tab](https://github.com/wljince007/tabby-sftp-tab) - 为ssh连接打开类似SecureCRT的sftp标签页
* [web-auth-handler](https://github.com/Jazzmoon/tabby-web-auth-handler) - 应用内网页认证弹出窗口主要为warpgate浏览器认证而建
* [mcp-server](https://github.com/thuanpham582002/tabby-mcp-server) - 为 Tabby 提供强大的模型上下文协议服务器集成,可通过 Cursor 和 Windsurf 等 MCP 客户端无缝连接 AI 助手,利用智能 AI 功能增强您的终端工作流程。
<a name="themes"></a>
# 主题
@ -337,6 +338,12 @@
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fireblue"><img src="https://avatars.githubusercontent.com/u/1034929?v=4?s=100" width="100px;" alt="fireblue"/><br /><sub><b>fireblue</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=fireblue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marko1616"><img src="https://avatars.githubusercontent.com/u/45327989?v=4?s=100" width="100px;" alt="marko1616"/><br /><sub><b>marko1616</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=marko1616" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.selfhosted.sg/"><img src="https://avatars.githubusercontent.com/u/128927132?v=4?s=100" width="100px;" alt="SelfHosted"/><br /><sub><b>SelfHosted</b></sub></a><br /><a href="#financial-SelfHosted-Club" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hiroga.hatenablog.com/"><img src="https://avatars.githubusercontent.com/u/13391129?v=4?s=100" width="100px;" alt="Hiroaki Ogasawara"/><br /><sub><b>Hiroaki Ogasawara</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=xhiroga" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://p.foundation/"><img src="https://avatars.githubusercontent.com/u/80860929?v=4?s=100" width="100px;" alt="P Foundation"/><br /><sub><b>P Foundation</b></sub></a><br /><a href="#financial-pfoundation" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/et304383"><img src="https://avatars.githubusercontent.com/u/2693414?v=4?s=100" width="100px;" alt="et304383"/><br /><sub><b>et304383</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=et304383" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -1,7 +1,7 @@
import * as glasstron from 'glasstron'
import { autoUpdater } from 'electron-updater'
import { Subject, Observable, debounceTime } from 'rxjs'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents } from 'electron'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents, nativeTheme } from 'electron'
import ElectronConfig = require('electron-config')
import { enable as enableRemote } from '@electron/remote/main'
import * as os from 'os'
@ -100,6 +100,10 @@ export class Window {
}
}
if (process.platform === 'darwin') {
bwOptions.visualEffectState = 'active'
}
if (process.platform === 'darwin') {
this.window = new BrowserWindow(bwOptions) as GlasstronWindow
} else {
@ -115,6 +119,8 @@ export class Window {
this.setVibrancy(true)
}
this.setDarkMode(this.configStore.appearance?.colorSchemeMode ?? 'dark')
if (!options.hidden) {
if (maximized) {
this.window.maximize()
@ -201,6 +207,18 @@ export class Window {
}
}
setDarkMode (mode: string): void {
if (process.platform === 'darwin') {
if ('light' === mode ) {
nativeTheme.themeSource = 'light'
} else if ('auto' === mode) {
nativeTheme.themeSource = 'system'
} else {
nativeTheme.themeSource = 'dark'
}
}
}
focus (): void {
this.window.focus()
}
@ -373,6 +391,10 @@ export class Window {
this.setVibrancy(enabled, type)
})
this.on('window-set-dark-mode', (_, mode) => {
this.setDarkMode(mode)
})
this.on('window-set-window-controls-color', (_, theme) => {
if (process.platform === 'win32') {
const symbolColor: string = theme.foreground

View File

@ -4,7 +4,7 @@
"private": true,
"repository": "https://github.com/Eugeny/tabby",
"author": {
"name": "Eugene Pankov",
"name": "Tabby Developers",
"email": "e@ajenti.org"
},
"main": "dist/main.js",
@ -16,7 +16,7 @@
},
"dependencies": {
"@electron/remote": "^2",
"node-pty": "^1.1.0-beta.14",
"node-pty": "^1.1.0-beta39",
"any-promise": "^1.3.0",
"electron-config": "2.0.0",
"electron-debug": "^3.2.0",
@ -30,19 +30,19 @@
"native-process-working-directory": "^1.0.2",
"npm": "6",
"rxjs": "^7.5.7",
"russh": "0.0.3",
"russh": "0.1.30",
"source-map-support": "^0.5.20",
"v8-compile-cache": "^2.3.0",
"yargs": "^17.7.2"
},
"optionalDependencies": {
"@tabby-gang/windows-blurbehind": "^3.0.0",
"@tabby-gang/windows-blurbehind": "^3.1.0",
"macos-native-processlist": "^2.1.0",
"patch-package": "^6.5.0",
"patch-package": "^8",
"serialport": "11.0.1",
"serialport-binding-webserialapi": "^1.0.3",
"windows-native-registry": "^3.2.1",
"windows-process-tree": "^0.4.0"
"@tabby-gang/windows-process-tree": "^0.6.1"
},
"devDependencies": {
"@ngx-translate/core": "^14.0.0",
@ -64,7 +64,9 @@
"tabby-terminal": "*"
},
"resolutions": {
"*/node-abi": "^3",
"node-gyp": "^10.0.0"
"node-abi": "4.9.0",
"node-gyp": "^10.0.0",
"nan": "2.22.2",
"node-addon-api": "^8.3.0"
}
}

View File

@ -0,0 +1,29 @@
diff --git a/node_modules/node-pty/src/win/conpty.cc b/node_modules/node-pty/src/win/conpty.cc
index c41796c..08c6439 100644
--- a/node_modules/node-pty/src/win/conpty.cc
+++ b/node_modules/node-pty/src/win/conpty.cc
@@ -107,9 +107,22 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
CloseHandle(baton->hOut);
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
- assert(status == napi_ok);
+ switch (status) {
+ case napi_closing:
+ break;
- tsfn.Release();
+ case napi_queue_full:
+ Napi::Error::Fatal("SetupExitCallback", "Queue was full");
+
+ case napi_ok:
+ if (tsfn.Release() != napi_ok) {
+ Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
+ }
+ break;
+
+ default:
+ Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
+ }
});
}

View File

@ -6,4 +6,6 @@ export const PLUGIN_BLACKLIST = [
'terminus-clickable-ips', // broken, functionality now bundled with Tabby
'terminus-elastic-quick-commands', // broken and abandoned, fork of quick-commands
'terminus-elastic-quick-cmds', // broken and abandoned, fork of quick-commands
'tabby-fig', // abandoned,
'tabby-plugin-fig-integration', // abandoned,
]

View File

@ -178,13 +178,20 @@
dependencies:
debug "^4.3.2"
"@tabby-gang/windows-blurbehind@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@tabby-gang/windows-blurbehind/-/windows-blurbehind-3.0.0.tgz#48d409c2eb14a12c867b70de5ee4d6769ef45e8f"
integrity sha512-ah6eJcoQZWOZfu9sd2pWlOJmfl1v+2EZQMeIp7MWvg+/16WS16UFNdnOtlV6AUiABHfZo2QKfCNUEuorCM+Q2A==
"@tabby-gang/windows-blurbehind@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@tabby-gang/windows-blurbehind/-/windows-blurbehind-3.1.0.tgz#bd1462f2a970d195e277d48cceb8b2c0a20f09bd"
integrity sha512-wTvyNrBDNxD4yq1bXv7lvXRQujJYRQ2Ke3LJyE9yzY9e/L9/fHVIuprIgAMiLsnW+BKPERq0k27iC38WnxUBZQ==
dependencies:
"@types/node" "^10.12.18"
"@tabby-gang/windows-process-tree@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@tabby-gang/windows-process-tree/-/windows-process-tree-0.6.1.tgz#2a5a5cbbee16f611fc61d53cbadf930c8d3d20a8"
integrity sha512-I7AwncTXTmo1+WPfV+O+jYRJzjCMJznIjC/ycl4dP/n2HAocuXCIjTZfoMmL+rgjN2nRFpTyn6P+EhuIPMACbQ==
dependencies:
node-addon-api "7.1.0"
"@types/mz@2.7.4":
version "2.7.4"
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
@ -397,6 +404,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz"
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:
version "2.0.2"
resolved "https://registry.yarnpkg.com/atomically/-/atomically-2.0.2.tgz#e5a6e8021441405b7a1c36d4587e25f7a13545f2"
@ -568,6 +580,14 @@ cacache@^18.0.0:
tar "^6.1.11"
unique-filename "^3.0.0"
call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
call-bind@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
@ -576,6 +596,24 @@ call-bind@^1.0.0:
function-bind "^1.1.1"
get-intrinsic "^1.0.0"
call-bind@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
dependencies:
call-bind-apply-helpers "^1.0.0"
es-define-property "^1.0.0"
get-intrinsic "^1.2.4"
set-function-length "^1.2.2"
call-bound@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
call-limit@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4"
@ -638,6 +676,11 @@ ci-info@^2.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.7.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
cidr-regex@^2.0.10:
version "2.0.10"
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.10.tgz#af13878bd4ad704de77d6dc800799358b3afa70d"
@ -846,17 +889,6 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -866,6 +898,15 @@ cross-spawn@^7.0.0:
shebang-command "^2.0.0"
which "^2.0.1"
cross-spawn@^7.0.3:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
crypto-random-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
@ -938,6 +979,15 @@ defaults@^1.0.3:
dependencies:
clone "^1.0.2"
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@ -990,6 +1040,15 @@ dotenv@^5.0.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@ -1164,6 +1223,23 @@ es-abstract@^1.18.0-next.1:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-define-property@^1.0.0, es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -1348,14 +1424,15 @@ fs-extra@^10.0.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
fs-extra@^9.0.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
at-least-node "^1.0.0"
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^1.2.7:
version "1.2.7"
@ -1407,6 +1484,11 @@ function-bind@^1.1.1:
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz"
@ -1462,6 +1544,30 @@ get-intrinsic@^1.0.0:
has "^1.0.3"
has-symbols "^1.0.1"
get-intrinsic@^1.2.4, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@ -1525,6 +1631,11 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
got@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
@ -1580,11 +1691,23 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-unicode@^2.0.0, has-unicode@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz"
@ -1597,6 +1720,13 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -1778,13 +1908,6 @@ is-ci@^1.0.10:
dependencies:
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-3.1.1.tgz#e92ef121bdec2782271a77ce487a8b8df3718ab7"
@ -1917,6 +2040,11 @@ isarray@0.0.1:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz"
@ -1978,18 +2106,22 @@ json-schema@0.2.3:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-stable-stringify@^1.0.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70"
integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==
dependencies:
call-bind "^1.0.8"
call-bound "^1.0.4"
isarray "^2.0.5"
jsonify "^0.0.1"
object-keys "^1.1.1"
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
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:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -1999,6 +2131,11 @@ jsonfile@^6.0.1:
optionalDependencies:
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:
version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@ -2374,6 +2511,11 @@ make-fetch-happen@^5.0.0:
socks-proxy-agent "^4.0.0"
ssri "^6.0.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
meant@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c"
@ -2600,10 +2742,10 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.17.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.20.0.tgz#08c5ea813dd54ed16e5bd6505bf42af4f7838ca3"
integrity sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==
nan@2.22.2:
version "2.22.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb"
integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==
napi-build-utils@^1.0.1:
version "1.0.2"
@ -2629,44 +2771,17 @@ ngx-filesize@^3.0.2:
dependencies:
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@^3:
version "3.65.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.65.0.tgz#ca92d559388e1e9cab1680a18c1a18757cdac9d3"
integrity sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==
node-abi@4.9.0, node-abi@^3.3.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.9.0.tgz#ca6dabf7991e54bf3ba6d8d32641e1b84f305263"
integrity sha512-0isb3h+AXUblx5Iv0mnYy2WsErH+dk2e9iXJXdKAtS076Q5hP+scQhp6P4tvDeVlOBlG3ROKvkpQHtbORllq2A==
dependencies:
semver "^7.3.5"
semver "^7.6.3"
node-abi@^3.3.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32"
integrity sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==
dependencies:
semver "^7.3.5"
node-addon-api@3.1.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-addon-api@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
node-addon-api@^4.0.0, node-addon-api@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
node-addon-api@^7.1.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-addon-api@3.1.0, node-addon-api@6.1.0, node-addon-api@7.1.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0, node-addon-api@^4.0.0, node-addon-api@^4.3.0, node-addon-api@^7.1.0, node-addon-api@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.3.0.tgz#ec3763f18befc1cdf66d11e157ce44d5eddc0603"
integrity sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==
node-fetch-npm@^2.0.2:
version "2.0.4"
@ -2698,7 +2813,7 @@ node-gyp@^10.0.0, node-gyp@^5.0.2, node-gyp@^5.1.0:
tar "^6.1.2"
which "^4.0.0"
node-pty@^1.1.0-beta.14:
node-pty@^1.1.0-beta39:
version "1.1.0-beta9"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta9.tgz#ed643cb3b398d031b4e31c216e8f3b0042435f1d"
integrity sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==
@ -3192,25 +3307,26 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
patch-package@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.5.0.tgz#feb058db56f0005da59cfa316488321de585e88a"
integrity sha512-tC3EqJmo74yKqfsMzELaFwxOAu6FH6t+FzFOsnWAuARm7/n2xB5AOeOueE221eM9gtMuIKMKpF9tBy/X2mNP0Q==
patch-package@^8:
version "8.0.0"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
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"
fs-extra "^7.0.1"
is-ci "^2.0.0"
fs-extra "^9.0.0"
json-stable-stringify "^1.0.2"
klaw-sync "^6.0.0"
minimist "^1.2.6"
open "^7.4.2"
rimraf "^2.6.3"
semver "^5.6.0"
semver "^7.5.3"
slash "^2.0.0"
tmp "^0.0.33"
yaml "^1.10.2"
yaml "^2.2.2"
path-exists@^3.0.0:
version "3.0.0"
@ -3227,7 +3343,7 @@ 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"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
path-key@^2.0.0, path-key@^2.0.1:
path-key@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
@ -3636,10 +3752,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
russh@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/russh/-/russh-0.0.3.tgz#bcb53d2efbe2b216857171bc5ca2131001ddfa46"
integrity sha512-iTW4M5W856zYjbjQYjlDFaSFSQ6pLBy+zsCYFwhivYuj8U5mZ7kF7TeGOUat9t4l25HVxAS36ivCt5l79p9lcQ==
russh@0.1.30:
version "0.1.30"
resolved "https://registry.yarnpkg.com/russh/-/russh-0.1.30.tgz#a475a94bcb6a3d0042f3c2b315a89dbebcf70e71"
integrity sha512-Qy7rbKN/1wv2cgzmX4T3znwvUDVqATEkDY3YE4LSTeY+kIoL9OBuQqxFiUkdUXiw0Wqq9sjaOy4LIqkyZSMB9g==
dependencies:
"@napi-rs/cli" "^2.18.3"
@ -3677,7 +3793,7 @@ semver-diff@^2.0.0:
dependencies:
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"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -3689,6 +3805,11 @@ semver@^7.3.5:
dependencies:
lru-cache "^6.0.0"
semver@^7.5.3, semver@^7.6.3:
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
serialize-error@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac"
@ -3729,6 +3850,18 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
sha@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/sha/-/sha-3.0.0.tgz#b2f2f90af690c16a3a839a6a6c680ea51fedd1ae"
@ -3949,7 +4082,8 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -3984,15 +4118,6 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@ -4035,7 +4160,8 @@ stringify-package@^1.0.0, stringify-package@^1.0.1:
resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85"
integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -4070,13 +4196,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -4314,11 +4433,6 @@ unique-string@^1.0.0:
dependencies:
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
@ -4474,13 +4588,6 @@ windows-native-registry@^3.2.1:
dependencies:
node-addon-api "^3.1.0"
windows-process-tree@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.4.0.tgz#31ac49c5da557e628ce7e37a5800972173d3349a"
integrity sha512-9LunDnc1WwuhyLeTAXMFX8wbActGJtDCBaiapQXFYk/nO4W4X9YxOKV5g/lQL3XX69QYxveDbjVVrdnTt1qqCQ==
dependencies:
nan "^2.17.0"
worker-farm@^1.6.0, worker-farm@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
@ -4488,7 +4595,8 @@ worker-farm@^1.6.0, worker-farm@^1.7.0:
dependencies:
errno "~0.1.7"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -4514,15 +4622,6 @@ wrap-ansi@^5.1.0:
string-width "^3.0.0"
strip-ansi "^5.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@ -4593,10 +4692,10 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.2.2:
version "2.8.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79"
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
yargs-parser@^15.0.1:
version "15.0.1"

View File

@ -8,7 +8,7 @@
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.microphone</key>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>

View File

@ -37,7 +37,8 @@ asarUnpack:
win:
icon: "./build/windows/icon.ico"
artifactName: tabby-${version}-portable-${env.ARCH}.${ext}
rfc3161TimeStampServer: http://timestamp.sectigo.com
signtoolOptions:
rfc3161TimeStampServer: http://timestamp.sectigo.com
nsis:
oneClick: false
artifactName: tabby-${version}-setup-${env.ARCH}.${ext}
@ -71,7 +72,8 @@ linux:
executableArgs:
- "--no-sandbox"
desktop:
StartupWMClass: tabby
entry:
StartupWMClass: tabby
snap:
plugs:
- default
@ -100,3 +102,7 @@ rpm:
- '_build_id_links none'
- '--replaces'
- 'terminus-terminal'
electronFuses:
runAsNode: false
enableNodeOptionsEnvironmentVariable: false
enableNodeCliInspectArguments: false

View File

@ -1,32 +1,32 @@
# When this file is named "default_settings" and is in the binaries
# directory or profile directory, it provides enhanced default settings.
# Override built-in default settings with ones that provide a more
# enhanced Clink experience.
clink.default_bindings = windows
cmd.ctrld_exits = False
color.arginfo = sgr 38;5;172
color.argmatcher = sgr 1;38;5;40
color.cmd = bold
color.cmdredir = sgr 38;5;172
color.cmdsep = sgr 38;5;135
color.comment_row = sgr 38;5;87;48;5;18
color.description = sgr 38;5;39
color.doskey = sgr 1;38;5;75
color.executable = sgr 1;38;5;33
color.filtered = bold
color.flag = sgr 38;5;117
color.hidden = sgr 38;5;160
color.histexpand = sgr 97;48;5;55
color.horizscroll = sgr 38;5;16;48;5;30
color.input = sgr 38;5;214
color.readonly = sgr 38;5;28
color.selected_completion = sgr 7
color.selection = sgr 38;5;16;48;5;179
color.unrecognized = sgr 38;5;203
history.max_lines = 25000
history.time_stamp = show
match.expand_envvars = True
match.substring = True
# When this file is named "default_settings" and is in the binaries
# directory or profile directory, it provides enhanced default settings.
# Override built-in default settings with ones that provide a more
# enhanced Clink experience.
clink.default_bindings = windows
clink.autoupdate = off
cmd.ctrld_exits = False
color.arginfo = sgr 38;5;172
color.argmatcher = sgr 1;38;5;40
color.cmd = bold
color.cmdredir = sgr 38;5;172
color.cmdsep = sgr 38;5;135
color.comment_row = sgr 38;5;87;48;5;18
color.description = sgr 38;5;39
color.doskey = sgr 1;38;5;75
color.executable = sgr 1;38;5;33
color.filtered = bold
color.flag = sgr 38;5;117
color.hidden = sgr 38;5;160
color.histexpand = sgr 97;48;5;55
color.horizscroll = sgr 38;5;16;48;5;30
color.input = sgr 38;5;214
color.readonly = sgr 38;5;28
color.selected_completion = sgr 7
color.selection = sgr 38;5;16;48;5;179
color.unrecognized = sgr 38;5;203
history.max_lines = 25000
history.time_stamp = show
match.expand_envvars = True
match.substring = True

View File

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: tabby\n"
"Language-Team: Indonesian\n"
"Language: id_ID\n"
"PO-Revision-Date: 2024-12-24 22:58\n"
"PO-Revision-Date: 2025-01-22 22:02\n"
#: tabby-local/src/components/terminalTab.component.ts:113
msgid "\"{command}\" is still running. Close?"
@ -23,7 +23,7 @@ msgstr "{name} salin"
#: locale/tmp-html/tabby-terminal/src/components/appearanceSettingsTab.component.html:77
msgid "A second font family used to display characters missing in the main font"
msgstr "Keluarga font kedua digunakan untuk menampilkan karakter yang hilang di font utama"
msgstr "Keluarga huruf kedua digunakan untuk menampilkan karakter yang hilang di huruf utama"
#: tabby-core/src/components/transfersMenu.component.ts:49
msgid "Abort all"
@ -100,12 +100,12 @@ msgstr "Izinkan buka dengan cepat terminal di direktori terpilih"
#: locale/tmp-html/tabby-core/src/components/welcomeTab.component.html:25
#: locale/tmp-html/tabby-terminal/src/components/colorSchemeSettingsTab.component.html:11
msgid "Always dark"
msgstr ""
msgstr "Selalu gelap"
#: locale/tmp-html/tabby-core/src/components/welcomeTab.component.html:27
#: locale/tmp-html/tabby-terminal/src/components/colorSchemeSettingsTab.component.html:13
msgid "Always light"
msgstr ""
msgstr "Selalu terang"
#: locale/tmp-html/tabby-terminal/src/components/appearanceSettingsTab.component.html:2
#: tabby-terminal/src/settings.ts:14
@ -255,7 +255,7 @@ msgstr "Ubah baud rate"
#: tabby-core/src/tabContextMenu.ts:133
msgid "Change tab color"
msgstr ""
msgstr "Rubah warna label"
#: locale/tmp-html/tabby-settings/src/components/vaultSettingsTab.component.html:12
msgid "Change the master passphrase"
@ -337,7 +337,7 @@ msgstr "Skema warna"
#: locale/tmp-html/tabby-terminal/src/components/colorSchemeSettingsTab.component.html:2
msgid "Color schemes"
msgstr ""
msgstr "Warna Skema"
#: locale/tmp-html/tabby-serial/src/components/serialProfileSettings.component.html:81
#: locale/tmp-html/tabby-ssh/src/components/sshProfileSettings.component.html:216
@ -362,7 +362,7 @@ msgstr "Perintah-perintah"
#: tabby-core/src/theme.ts:16
msgid "Compact (legacy)"
msgstr ""
msgstr "Padat (tua)"
#: tabby-settings/src/components/configSyncSettingsTab.component.ts:126
msgid "Config deleted"
@ -449,7 +449,7 @@ msgstr "Salin jalur saat ini"
#: tabby-electron/src/sftpContextMenu.ts:29
msgid "Copy full path"
msgstr ""
msgstr "Salin alamat lengkap"
#: locale/tmp-html/tabby-terminal/src/components/terminalSettingsTab.component.html:97
msgid "Copy on select"
@ -1732,7 +1732,7 @@ msgstr "Ganti Nama"
#: tabby-core/src/hotkeys.ts:24
#: tabby-core/src/tabContextMenu.ts:121
msgid "Rename tab"
msgstr ""
msgstr "Ganti nama label"
#: locale/tmp-html/tabby-terminal/src/components/terminalSettingsTab.component.html:3
msgid "Rendering"
@ -1770,7 +1770,7 @@ msgstr "Mulai ulang sesi SSH saat ini"
#: tabby-telnet/src/hotkeys.ts:10
msgid "Restart current Telnet session"
msgstr ""
msgstr "Mulai ulang sesi Telnet saat ini"
#: tabby-core/src/hotkeys.ts:64
msgid "Restart tab"
@ -1782,7 +1782,7 @@ msgstr "Mulai ulang aplikasi untuk menerapkan perubahan"
#: tabby-settings/src/components/profilesSettingsTab.component.ts:316
msgid "Restore settings to defaults ?"
msgstr ""
msgstr "Kembali ke settingan sebelumnya ?"
#: tabby-settings/src/components/editProfileGroupModal.component.ts:36
msgid "Restore settings to inherited defaults ?"

View File

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: tabby\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"PO-Revision-Date: 2024-12-24 22:58\n"
"PO-Revision-Date: 2025-01-22 22:02\n"
#: tabby-local/src/components/terminalTab.component.ts:113
msgid "\"{command}\" is still running. Close?"
@ -2177,7 +2177,7 @@ msgstr "Cambia l'implementazione del frontend del terminale (sperimentale)"
#: locale/tmp-html/tabby-settings/src/components/configSyncSettingsTab.component.html:4
msgid "Sync"
msgstr "Sincronizazione"
msgstr "Sincronizzazione"
#: locale/tmp-html/tabby-settings/src/components/configSyncSettingsTab.component.html:53
msgid "Sync automatically"

View File

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: tabby\n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
"PO-Revision-Date: 2024-12-24 22:58\n"
"PO-Revision-Date: 2025-01-22 22:02\n"
#: tabby-local/src/components/terminalTab.component.ts:113
msgid "\"{command}\" is still running. Close?"
@ -966,7 +966,7 @@ msgstr "Hazır bir GitHub sorunu oluşturun"
#: locale/tmp-html/tabby-plugin-manager/src/components/pluginsSettingsTab.component.html:25
msgid "Get"
msgstr "Getir"
msgstr "Yükle"
#: locale/tmp-html/tabby-settings/src/components/configSyncSettingsTab.component.html:18
msgid "Get it from the Tabby Web settings window"

View File

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: tabby\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
"PO-Revision-Date: 2024-12-24 22:58\n"
"PO-Revision-Date: 2025-01-22 22:02\n"
#: tabby-local/src/components/terminalTab.component.ts:113
msgid "\"{command}\" is still running. Close?"

View File

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: tabby\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
"PO-Revision-Date: 2024-12-24 22:58\n"
"PO-Revision-Date: 2025-01-22 22:02\n"
#: tabby-local/src/components/terminalTab.component.ts:113
msgid "\"{command}\" is still running. Close?"

View File

@ -12,6 +12,7 @@
"@angular/platform-browser-dynamic": "^15.2.6",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
"@electron/notarize": "^1.2.3",
"@electron/rebuild": "^4",
"@fortawesome/fontawesome-free": "^6.4.0",
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
"@ngtools/webpack": "^15.2.5",
@ -39,11 +40,10 @@
"cross-env": "7.0.3",
"css-loader": "^6.7.3",
"deep-equal": "2.0.5",
"electron": "^29",
"electron-builder": "^24.6.4",
"electron": "38",
"electron-builder": "^26.0",
"electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0",
"@electron/rebuild": "^3.7.1",
"eslint": "^8.48.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.28.1",
@ -55,7 +55,7 @@
"lru-cache": "^6.0.0",
"macos-release": "^3.3.0",
"ngx-toastr": "^16.0.2",
"node-abi": "^3.65.0",
"node-abi": "^4",
"npmlog": "6.0.2",
"npx": "^10.2.2",
"patch-package": "^6.4.7",
@ -96,7 +96,8 @@
"*/pug": "^3",
"lzma-native": "^8.0.6",
"**/graceful-fs": "^4.2.4",
"nan": "2.17.0",
"nan": "2.22.2",
"node-abi": "^4",
"node-gyp": "^10.0.0"
},
"scripts": {

View File

@ -1,8 +1,8 @@
diff --git a/node_modules/app-builder-lib/out/appInfo.js b/node_modules/app-builder-lib/out/appInfo.js
index 49f6dca..0ea11f2 100644
index d159c17..eb48466 100644
--- a/node_modules/app-builder-lib/out/appInfo.js
+++ b/node_modules/app-builder-lib/out/appInfo.js
@@ -112,9 +112,7 @@ class AppInfo {
@@ -116,9 +116,7 @@ class AppInfo {
return this.info.metadata.name;
}
get linuxPackageName() {

View File

@ -28,10 +28,7 @@ builder({
},
mac: {
identity: !process.env.CI || process.env.CSC_LINK ? undefined : null,
notarize: process.env.APPLE_TEAM_ID ? {
appBundleId: 'org.tabby',
teamId: process.env.APPLE_TEAM_ID,
} : false,
notarize: !!process.env.APPLE_TEAM_ID,
},
npmRebuild: process.env.ARCH !== 'arm64',
publish: process.env.KEYGEN_TOKEN ? [

View File

@ -28,27 +28,35 @@ builder({
] : undefined,
forceCodeSigning: !!keypair,
win: {
certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
publisherName: process.env.SM_PUBLISHER_NAME,
signingHashAlgorithms: ['sha256'],
sign: keypair ? async function (configuration) {
console.log('Signing', configuration)
if (configuration.path) {
try {
const out = execSync(
`smctl sign --keypair-alias=${keypair} --input "${String(configuration.path)}"`
)
if (out.toString().includes('FAILED')) {
throw new Error(out.toString())
signtoolOptions: {
certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
publisherName: process.env.SM_PUBLISHER_NAME,
signingHashAlgorithms: ['sha256'],
sign: keypair ? async function (configuration) {
console.log('Signing', configuration)
if (configuration.path) {
try {
const cmd = `smctl sign --keypair-alias=${keypair} --input "${String(configuration.path)}"`
console.log(cmd)
const out = execSync(cmd)
if (out.toString().includes('FAILED')) {
throw new Error(out.toString())
}
console.log(out.toString())
} catch (e) {
console.error(`Failed to sign ${configuration.path}`)
if (e.stdout) {
console.error('stdout:', e.stdout.toString())
}
if (e.stderr) {
console.error('stderr:', e.stderr.toString())
}
console.error(e)
process.exit(1)
}
console.log(out.toString())
} catch (e) {
console.error(`Failed to sign ${configuration.path}`)
console.error(e)
process.exit(1)
}
}
} : undefined,
} : undefined,
},
},
},

View File

@ -10,6 +10,8 @@ log.info('deps', 'app')
sh.cd('app')
sh.exec(`yarn install --force --network-timeout 1000000`, { fatal: true })
// Some native packages might fail to build before patch-package gets a chance to run via postinstall
sh.exec(`yarn postinstall`, { fatal: false })
sh.cd('..')
sh.cd('web')

View File

@ -31,6 +31,7 @@ export const builtinPlugins = [
'tabby-electron',
'tabby-plugin-manager',
'tabby-linkifier',
'tabby-auto-sudo-password',
]
export const packagesWithDocs = [

View File

@ -0,0 +1,23 @@
{
"name": "tabby-auto-sudo-password",
"version": "1.0.197-nightly.1",
"description": "Offers to automatically paste saved sudo password in SSH sessions",
"keywords": [
"tabby-builtin-plugin"
],
"main": "dist/index.js",
"typings": "typings/index.d.ts",
"scripts": {
"build": "webpack --progress --color --display-modules",
"watch": "webpack --progress --color --watch"
},
"files": [
"dist",
"typings"
],
"devDependencies": {
"ansi-colors": "^4.1.1"
},
"author": "Tabby Developers",
"license": "MIT"
}

View File

@ -0,0 +1,89 @@
import colors from 'ansi-colors'
import { Injectable } from '@angular/core'
import { TerminalDecorator, BaseTerminalTabComponent, XTermFrontend, SessionMiddleware } from 'tabby-terminal'
import { SSHProfile, SSHTabComponent, PasswordStorageService } from 'tabby-ssh'
const SUDO_PROMPT_REGEX = /^\[sudo\] password for ([^:]+):\s*$/im
export class AutoSudoPasswordMiddleware extends SessionMiddleware {
private pendingPasswordToPaste: string | null = null
private pasteHint = `${colors.black.bgBlackBright(' Tabby ')} ${colors.gray('Press Enter to paste saved password')}`
private pasteHintLength = colors.stripColor(this.pasteHint).length
constructor (
private profile: SSHProfile,
private ps: PasswordStorageService,
) { super() }
feedFromSession (data: Buffer): void {
const text = data.toString('utf-8')
const match = SUDO_PROMPT_REGEX.exec(text)
if (match) {
const username = match[1]
this.handlePrompt(username)
}
this.outputToTerminal.next(data)
}
feedFromTerminal (data: Buffer): void {
if (this.pendingPasswordToPaste) {
const backspaces = Buffer.alloc(this.pasteHintLength, 8) // backspace
const spaces = Buffer.alloc(this.pasteHintLength, 32) // space
const clear = Buffer.concat([backspaces, spaces, backspaces])
this.outputToTerminal.next(clear)
if (data.length === 1 && data[0] === 13) { // Enter key
this.outputToSession.next(Buffer.from(this.pendingPasswordToPaste + '\n'))
this.pendingPasswordToPaste = null
return
} else {
this.pendingPasswordToPaste = null
}
}
this.outputToSession.next(data)
}
async handlePrompt (username: string): Promise<void> {
console.log(`Detected sudo prompt for user: ${username}`)
const pw = await this.ps.loadPassword(this.profile, username)
if (pw) {
this.outputToTerminal.next(Buffer.from(this.pasteHint))
this.pendingPasswordToPaste = pw
}
}
async loadPassword (username: string): Promise<string| null> {
if (this.profile.options.user !== username) {
return null
}
return this.ps.loadPassword(this.profile, username)
}
}
@Injectable()
export class AutoSudoPasswordDecorator extends TerminalDecorator {
constructor (
private ps: PasswordStorageService,
) {
super()
}
private attachToSession (tab: SSHTabComponent) {
if (!tab.session) {
return
}
tab.session.middleware.unshift(new AutoSudoPasswordMiddleware(tab.profile, this.ps))
}
attach (tab: BaseTerminalTabComponent<any>): void {
if (!(tab.frontend instanceof XTermFrontend) || !(tab instanceof SSHTabComponent)) {
return
}
setTimeout(() => {
this.attachToSession(tab)
this.subscribeUntilDetached(tab, tab.sessionChanged$.subscribe(() => {
this.attachToSession(tab)
}))
})
}
}

View File

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core'
import { ToastrModule } from 'ngx-toastr'
import { TerminalDecorator } from 'tabby-terminal'
import { AutoSudoPasswordDecorator } from './decorator'
@NgModule({
imports: [
ToastrModule,
],
providers: [
{ provide: TerminalDecorator, useClass: AutoSudoPasswordDecorator, multi: true },
],
})
export default class AutoSudoPasswordModule { }

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "src",
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"compilerOptions": {
"baseUrl": "src",
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./typings",
"paths": {
"tabby-*": ["../../tabby-*"],
"*": ["../../app/node_modules/*"]
}
}
}

View File

@ -0,0 +1,10 @@
import * as path from 'path'
import * as url from 'url'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
import config from '../webpack.plugin.config.mjs'
export default () => config({
name: 'auto-sudo-password',
dirname: __dirname,
})

View File

@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-colors@^4.1.1:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==

View File

@ -14,7 +14,7 @@
"files": [
"dist"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"peerDependencies": {
"@angular/core": "^15",

View File

@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #c0caf5
*.background: #1a1b26
*.cursorColor: #c0caf5
!
! Black
*.color0: #15161e
*.color8: #414868
!
! Red
*.color1: #f7768e
*.color9: #f7768e
!
! Green
*.color2: #9ece6a
*.color10: #9ece6a
!
! Yellow
*.color3: #e0af68
*.color11: #e0af68
!
! Blue
*.color4: #7aa2f7
*.color12: #7aa2f7
!
! Magenta
*.color5: #bb9af7
*.color13: #bb9af7
!
! Cyan
*.color6: #7dcfff
*.color14: #7dcfff
!
! White
*.color7: #a9b1d6
*.color15: #c0caf5
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #3760bf
*.background: #e1e2e7
*.cursorColor: #3760bf
!
! Black
*.color0: #e9e9ed
*.color8: #a1a6c5
!
! Red
*.color1: #f52a65
*.color9: #f52a65
!
! Green
*.color2: #587539
*.color10: #587539
!
! Yellow
*.color3: #8c6c3e
*.color11: #8c6c3e
!
! Blue
*.color4: #2e7de9
*.color12: #2e7de9
!
! Magenta
*.color5: #9854f1
*.color13: #9854f1
!
! Cyan
*.color6: #007197
*.color14: #007197
!
! White
*.color7: #6172b0
*.color15: #3760bf
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@ -0,0 +1,44 @@
!
! Generated with :
! XRDB2Xreources.py
!
*.foreground: #c0caf5
*.background: #24283b
*.cursorColor: #c0caf5
!
! Black
*.color0: #1d202f
*.color8: #414868
!
! Red
*.color1: #f7768e
*.color9: #f7768e
!
! Green
*.color2: #9ece6a
*.color10: #9ece6a
!
! Yellow
*.color3: #e0af68
*.color11: #e0af68
!
! Blue
*.color4: #7aa2f7
*.color12: #7aa2f7
!
! Magenta
*.color5: #bb9af7
*.color13: #bb9af7
!
! Cyan
*.color6: #7dcfff
*.color14: #7dcfff
!
! White
*.color7: #a9b1d6
*.color15: #c0caf5
!
! Bold, Italic, Underline
*.colorBD: #eeeeee
!*.colorIT:
!*.colorUL:

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"bootstrap": "^5.3.0-alpha.1",

View File

@ -10,7 +10,7 @@ export { Theme } from './theme'
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
export { SelectorOption } from './selector'
export { CLIHandler, CLIEvent } from './cli'
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload } from './platform'
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload, DirectoryDownload, PlatformTheme } from './platform'
export { MenuItemOptions } from './menu'
export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow'
@ -36,7 +36,7 @@ export { TabsService, NewTabParameters, TabComponentType } from '../services/tab
export { UpdaterService } from '../services/updater.service'
export { VaultService, Vault, VaultSecret, VaultFileSecret, VAULT_SECRET_TYPE_FILE, StoredVault, VaultSecretKey } from '../services/vault.service'
export { FileProvidersService } from '../services/fileProviders.service'
export { LocaleService } from '../services/locale.service'
export { LocaleService, TabbyFormatedDatePipe } from '../services/locale.service'
export { TranslateService } from '@ngx-translate/core'
export * from '../utils'
export { UTF8Splitter } from '../utfSplitter'

View File

@ -22,7 +22,6 @@ export interface MessageBoxResult {
export abstract class FileTransfer {
abstract getName (): string
abstract getMode (): number
abstract getSize (): number
abstract close (): void
@ -34,8 +33,16 @@ export abstract class FileTransfer {
return this.completedBytes
}
getStatus (): string {
return this.status
}
getTotalSize (): number {
return this.totalSize
}
isComplete (): boolean {
return this.completedBytes >= this.getSize()
return this.completed || this.completedBytes >= this.getSize()
}
isCancelled (): boolean {
@ -47,6 +54,18 @@ export abstract class FileTransfer {
this.close()
}
setStatus (status: string): void {
this.status = status
}
setTotalSize (size: number): void {
this.totalSize = size
}
setCompleted (completed: boolean): void {
this.completed = completed
}
protected increaseProgress (bytes: number): void {
if (!bytes) {
return
@ -57,16 +76,26 @@ export abstract class FileTransfer {
}
private completedBytes = 0
private totalSize = 0
private lastChunkStartTime = Date.now()
private lastChunkSpeed = 0
private cancelled = false
private completed = false
private status = ''
}
export abstract class FileDownload extends FileTransfer {
abstract write (buffer: Uint8Array): Promise<void>
}
export abstract class DirectoryDownload extends FileTransfer {
abstract createDirectory (relativePath: string): Promise<void>
abstract createFile (relativePath: string, mode: number, size: number): Promise<FileDownload>
}
export abstract class FileUpload extends FileTransfer {
abstract getMode (): number
abstract read (): Promise<Uint8Array>
async readAll (): Promise<Uint8Array> {
@ -127,6 +156,7 @@ export abstract class PlatformService {
abstract saveConfig (content: string): Promise<void>
abstract startDownload (name: string, mode: number, size: number): Promise<FileDownload|null>
abstract startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null>
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
abstract startUploadDirectory (paths?: string[]): Promise<DirectoryUpload>
@ -237,7 +267,7 @@ export abstract class PlatformService {
abstract setErrorHandler (handler: (_: any) => void): void
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
abstract pickDirectory (): Promise<string>
abstract pickDirectory (): Promise<string | null>
abstract quit (): void
}

View File

@ -35,7 +35,8 @@ title-bar(
[@animateTab]='{value: "in", params: {size: targetTabSize}}',
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
(click)='app.selectTab(tab)',
[class.fully-draggable]='hostApp.platform !== Platform.macOS'
[class.fully-draggable]='hostApp.platform !== Platform.macOS',
[ngbTooltip]='tab.customTitle || tab.title'
)
.btn-group.background
@ -43,7 +44,7 @@ title-bar(
*ngFor='let button of leftToolbarButtons'
)
button.btn.btn-secondary.btn-tab-bar(
[title]='button.label',
[ngbTooltip]='button.label',
(click)='button.run && button.run()',
[fastHtmlBind]='button.icon'
)
@ -55,7 +56,7 @@ title-bar(
)
button.btn.btn-secondary.btn-tab-bar(
[hidden]='activeTransfers.length == 0',
title='File transfers',
[ngbTooltip]='"File transfers"|translate',
ngbDropdownToggle
) !{require('../icons/transfers.svg')}
transfers-menu(
@ -75,14 +76,14 @@ title-bar(
*ngFor='let button of rightToolbarButtons'
)
button.btn.btn-secondary.btn-tab-bar(
[title]='button.label',
[ngbTooltip]='button.label',
(click)='button.run && button.run()',
[fastHtmlBind]='button.icon'
)
button.btn.btn-secondary.btn-tab-bar.btn-update(
*ngIf='updatesAvailable',
title='Update available - Click to install',
[ngbTooltip]='"Update available - Click to install"|translate',
(click)='updater.update()'
) !{require('../icons/gift.svg')}

View File

@ -68,6 +68,7 @@ $tab-border-radius: 4px;
&>.inset {
opacity: 0;
width: 100%;
}
::ng-deep tab-header {

View File

@ -13,7 +13,6 @@ profile-icon(
)
.name(
[title]='tab.customTitle || tab.title',
[class.no-hover]='config.store.terminal.hideCloseButton && config.store.terminal.hideTabOptionsButton'
cdkDrag,
cdkDragRootElement='tab-header',

View File

@ -5,7 +5,9 @@
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
.main
label.no-wrap([title]='transfer.getName()') {{transfer.getName()}}
label.no-wrap([ngbTooltip]='transfer.getName()')
| {{transfer.getName()}}
span.ms-2.text-muted(*ngIf='transfer.getStatus()') ({{transfer.getStatus()}})
ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)')
.metadata
.size {{transfer.getSize()|filesize}}

View File

@ -2,7 +2,7 @@ import { NgModule, ModuleWithProviders, LOCALE_ID } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgbModule, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'
import { NgxFilesizeModule } from 'ngx-filesize'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { TranslateModule, TranslateCompiler, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'
@ -43,7 +43,7 @@ import { AppService } from './services/app.service'
import { ConfigService } from './services/config.service'
import { VaultFileProvider } from './services/vault.service'
import { HotkeysService } from './services/hotkeys.service'
import { CustomMissingTranslationHandler, LocaleService } from './services/locale.service'
import { CustomMissingTranslationHandler, LocaleService, TabbyFormatedDatePipe } from './services/locale.service'
import { CommandService } from './services/commands.service'
import { NewTheme } from './theme'
@ -130,6 +130,7 @@ const PROVIDERS = [
DropZoneDirective,
CdkAutoDropGroup,
ProfileIconComponent,
TabbyFormatedDatePipe,
],
exports: [
AppRootComponent,
@ -144,6 +145,7 @@ const PROVIDERS = [
TranslateModule,
CdkAutoDropGroup,
ProfileIconComponent,
TabbyFormatedDatePipe,
],
})
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
@ -153,6 +155,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
platform: PlatformService,
hotkeys: HotkeysService,
commands: CommandService,
ngbTooltipConfig: NgbTooltipConfig,
public locale: LocaleService,
private translate: TranslateService,
private profilesService: ProfilesService,
@ -199,6 +202,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
commands.run('core:profile-selector', {})
}
})
ngbTooltipConfig.openDelay = 750
ngbTooltipConfig.placement = 'top bottom auto'
ngbTooltipConfig.container = 'body'
}
async showSelector (provider: ProfileProvider<Profile>): Promise<void> {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { registerLocaleData } from '@angular/common'
import { Injectable, Pipe, PipeTransform } from '@angular/core'
import { formatDate, registerLocaleData } from '@angular/common'
import { TranslateService, MissingTranslationHandler } from '@ngx-translate/core'
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler'
@ -257,3 +257,15 @@ export class LocaleService {
return this.locale
}
}
@Pipe({
name: 'tabbyDate',
})
export class TabbyFormatedDatePipe implements PipeTransform {
constructor (private locale: LocaleService) {}
transform (date: string): string {
return formatDate(date, 'medium', this.locale.getLocale())
}
}

View File

@ -23,6 +23,7 @@ export class ThemesService {
) {
this.rootElementStyleBackup = document.documentElement.style.cssText
this.applyTheme(standardTheme)
this.applyThemeVariables()
config.ready$.toPromise().then(() => {
this.applyCurrentTheme()
this.applyThemeVariables()
@ -37,6 +38,11 @@ export class ThemesService {
})
}
private getConfigStoreOrDefaults (): any {
/// Theme service is active before the vault is unlocked and config is available
return this.config.store ?? this.config.getDefaults()
}
private applyThemeVariables () {
if (!this.findCurrentTheme().followsColorScheme) {
document.documentElement.style.cssText = this.rootElementStyleBackup
@ -60,7 +66,7 @@ export class ThemesService {
}
let background = Color(theme.background)
if (this.config.store?.appearance.vibrancy) {
if (this.getConfigStoreOrDefaults().appearance.vibrancy) {
background = background.fade(0.6)
}
// const background = theme.background
@ -148,13 +154,13 @@ export class ThemesService {
vars['--bs-form-switch-bg'] = `url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3e%3ccircle r=%273%27 fill=%27${switchBackground}%27/%3e%3c/svg%3e")`
}
vars['--spaciness'] = this.config.store.appearance.spaciness
vars['--spaciness'] = this.getConfigStoreOrDefaults().appearance.spaciness
for (const [bg, fg] of contrastPairs) {
const colorBg = Color(vars[bg]).hsl()
const colorFg = Color(vars[fg]).hsl()
const bgContrast = colorBg.contrast(colorFg)
if (bgContrast < this.config.store.terminal.minimumContrastRatio) {
if (bgContrast < this.getConfigStoreOrDefaults().terminal.minimumContrastRatio) {
vars[fg] = this.ensureContrast(colorFg, colorBg).string()
}
}
@ -163,7 +169,7 @@ export class ThemesService {
document.documentElement.style.setProperty(key, value)
}
document.body.classList.toggle('no-animations', !this.config.store.accessibility.animations)
document.body.classList.toggle('no-animations', !this.getConfigStoreOrDefaults().accessibility.animations)
}
private ensureContrast (color: Color, against: Color): Color {
@ -178,7 +184,7 @@ export class ThemesService {
while (
(step < 1 && color.color[2] > 1 ||
step > 1 && color.color[2] < 99) &&
color.contrast(against) < this.config.store.terminal.minimumContrastRatio) {
color.contrast(against) < this.getConfigStoreOrDefaults().terminal.minimumContrastRatio) {
color.color[2] *= step
}
return color
@ -189,22 +195,22 @@ export class ThemesService {
}
findCurrentTheme (): Theme {
return this.findTheme(this.config.store.appearance.theme) ?? this.standardTheme
return this.findTheme(this.getConfigStoreOrDefaults().appearance.theme) ?? this.standardTheme
}
/// @hidden
_getActiveColorScheme (): any {
let theme: PlatformTheme = 'dark'
if (this.config.store.appearance.colorSchemeMode === 'light') {
if (this.getConfigStoreOrDefaults().appearance.colorSchemeMode === 'light') {
theme = 'light'
} else if (this.config.store.appearance.colorSchemeMode === 'auto') {
} else if (this.getConfigStoreOrDefaults().appearance.colorSchemeMode === 'auto') {
theme = this.platform.getTheme()
}
if (theme === 'light') {
return this.config.store.terminal.lightColorScheme
return this.getConfigStoreOrDefaults().terminal.lightColorScheme
} else {
return this.config.store.terminal.colorScheme
return this.getConfigStoreOrDefaults().terminal.colorScheme
}
}
@ -215,7 +221,7 @@ export class ThemesService {
document.querySelector('head')!.appendChild(this.styleElement)
}
this.styleElement.textContent = theme.css
document.querySelector('style#custom-css')!.innerHTML = this.config.store?.appearance?.css
document.querySelector('style#custom-css')!.innerHTML = this.getConfigStoreOrDefaults().appearance.css
this.themeChanged.next(theme)
}

View File

@ -195,7 +195,13 @@ export class VaultService {
if (!vault) {
return null
}
return vault.secrets.find(s => s.type === type && this.keyMatches(key, s)) ?? null
let vaultSecret = vault.secrets.find(s => s.type === type && this.keyMatches(key, s))
if (!vaultSecret) {
// search for secret without host in vault (like a default user/password used in multiple servers)
key['host'] = null
vaultSecret = vault.secrets.find(s => s.type === type && this.keyMatches(key, s))
}
return vaultSecret ?? null
}
async addSecret (secret: VaultSecret): Promise<void> {

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"peerDependencies": {
"@angular/core": "^15",

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
import { TerminalColorSchemeProvider } from 'tabby-terminal'
import { TerminalColorSchemeProvider, TerminalDecorator } from 'tabby-terminal'
import { SFTPContextMenuItemProvider, SSHProfileImporter, AutoPrivateKeyLocator } from 'tabby-ssh'
import { PTYInterface, ShellProvider, UACService } from 'tabby-local'
import { auditTime } from 'rxjs'
@ -23,6 +23,7 @@ import { ElectronConfigProvider } from './config'
import { EditSFTPContextMenu } from './sftpContextMenu'
import { OpenSSHImporter, PrivateKeyLocator, StaticFileImporter } from './sshImporters'
import { ElectronPTYInterface } from './pty'
import { PathDropDecorator } from './pathDrop'
import { CmderShellProvider } from './shells/cmder'
import { Cygwin32ShellProvider } from './shells/cygwin32'
@ -73,6 +74,8 @@ import { VSDevToolsProvider } from './shells/vs'
{ provide: PTYInterface, useClass: ElectronPTYInterface },
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
// For WindowsDefaultShellProvider
PowerShellCoreShellProvider,
WSLShellProvider,
@ -130,7 +133,10 @@ export default class ElectronModule {
})
})
config.changed$.subscribe(() => this.updateVibrancy())
config.changed$.subscribe(() => {
this.updateVibrancy()
this.updateDarkMode()
})
config.changed$.subscribe(() => this.updateWindowControlsColor())
@ -173,6 +179,11 @@ export default class ElectronModule {
this.hostWindow.setOpacity(this.config.store.appearance.opacity)
}
private updateDarkMode () {
const colorSchemeMode = this.config.store.appearance.colorSchemeMode
this.electron.ipcRenderer.send('window-set-dark-mode', colorSchemeMode)
}
private updateWindowControlsColor () {
// if windows and not using native frame, WCO does not exist, return.
if (this.hostApp.platform === Platform.Windows && this.config.store.appearance.frame === 'native') {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { TerminalDecorator } from '../api/decorator'
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
import { TerminalDecorator, BaseTerminalTabComponent } from 'tabby-terminal'
import { webUtils } from 'electron'
/** @hidden */
@Injectable()
@ -11,8 +11,8 @@ export class PathDropDecorator extends TerminalDecorator {
event.preventDefault()
}))
this.subscribeUntilDetached(terminal, terminal.frontend?.drop$.subscribe((event: DragEvent) => {
for (const file of event.dataTransfer!.files as any) {
this.injectPath(terminal, file.path)
for (const file of event.dataTransfer!.files as unknown as Iterable<File>) {
this.injectPath(terminal, webUtils.getPathForFile(file))
}
event.preventDefault()
}))

View File

@ -10,7 +10,7 @@ try {
} catch { }
try {
var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
var windowsProcessTree = require('@tabby-gang/windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
export class ElectronPTYInterface extends PTYInterface {

View File

@ -50,7 +50,7 @@ export class DockMenuService {
])
}
if (this.hostApp.platform === Platform.macOS) {
this.electron.app.dock.setMenu(this.electron.Menu.buildFromTemplate(
this.electron.app.dock?.setMenu(this.electron.Menu.buildFromTemplate(
[
...[...this.profilesService.getRecentProfiles(), ...profiles].map(profile => ({
label: profile.name,

View File

@ -5,12 +5,11 @@ import * as os from 'os'
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core'
import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core'
import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, DirectoryDownload, FileUploadOptions, wrapPromise, TranslateService, FileTransfer, PlatformTheme } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
import { ElectronHostWindow } from './hostWindow.service'
import { ShellIntegrationService } from './shellIntegration.service'
import { ElectronHostAppService } from './hostApp.service'
import { PlatformTheme } from '../../../tabby-core/src/api/platform'
import { configPath } from '../../../app/lib/config'
const fontManager = require('fontmanager-redux') // eslint-disable-line
@ -18,7 +17,7 @@ const fontManager = require('fontmanager-redux') // eslint-disable-line
try {
// eslint-disable-next-line no-var
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node')
var windowsProcessTreeNative = require('@tabby-gang/windows-process-tree/build/Release/windows_process_tree.node')
// eslint-disable-next-line no-var
var wnr = require('windows-native-registry')
} catch { }
@ -272,19 +271,48 @@ export class ElectronPlatformService extends PlatformService {
return transfer
}
async startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null> {
const selectedFolder = await this.pickDirectory(this.translate.instant('Select destination folder for {name}', { name }), this.translate.instant('Download here'))
if (!selectedFolder) {
return null
}
let downloadPath = path.join(selectedFolder, name)
let counter = 1
while (fsSync.existsSync(downloadPath)) {
downloadPath = path.join(selectedFolder, `${name} (${counter})`)
counter++
}
const transfer = new ElectronDirectoryDownload(downloadPath, name, estimatedSize ?? 0, this.electron, this.zone)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
return transfer
}
_registerFileTransfer (transfer: FileTransfer): void {
this.fileTransferStarted.next(transfer)
}
setErrorHandler (handler: (_: any) => void): void {
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
handler(err)
})
}
async pickDirectory (): Promise<string> {
return (await this.electron.dialog.showOpenDialog(
async pickDirectory (title?: string, buttonLabel?: string): Promise<string | null> {
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
title,
buttonLabel,
properties: ['openDirectory', 'showHiddenFiles'],
},
)).filePaths[0]
)
if (result.canceled || !result.filePaths.length) {
return null
}
return result.filePaths[0]
}
getTheme (): PlatformTheme {
@ -313,6 +341,7 @@ class ElectronFileUpload extends FileUpload {
const stat = await fs.stat(this.filePath)
this.size = stat.size
this.mode = stat.mode
this.setTotalSize(this.size)
this.file = await fs.open(this.filePath, 'r')
}
@ -331,6 +360,9 @@ class ElectronFileUpload extends FileUpload {
async read (): Promise<Uint8Array> {
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
this.increaseProgress(result.bytesRead)
if (this.getCompletedBytes() >= this.getSize()) {
this.setCompleted(true)
}
return this.buffer.slice(0, result.bytesRead)
}
@ -352,6 +384,7 @@ class ElectronFileDownload extends FileDownload {
) {
super()
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
this.setTotalSize(size)
}
async open (): Promise<void> {
@ -362,10 +395,6 @@ class ElectronFileDownload extends FileDownload {
return path.basename(this.filePath)
}
getMode (): number {
return this.mode
}
getSize (): number {
return this.size
}
@ -377,6 +406,9 @@ class ElectronFileDownload extends FileDownload {
this.increaseProgress(result.bytesWritten)
pos += result.bytesWritten
}
if (this.getCompletedBytes() >= this.getSize()) {
this.setCompleted(true)
}
}
close (): void {
@ -384,3 +416,49 @@ class ElectronFileDownload extends FileDownload {
this.file.close()
}
}
class ElectronDirectoryDownload extends DirectoryDownload {
private powerSaveBlocker = 0
constructor (
private basePath: string,
private name: string,
estimatedSize: number,
private electron: ElectronService,
private zone: NgZone,
) {
super()
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
this.setTotalSize(estimatedSize)
}
async open (): Promise<void> {
await fs.mkdir(this.basePath, { recursive: true })
}
getName (): string {
return this.name
}
getSize (): number {
return this.getTotalSize()
}
async createDirectory (relativePath: string): Promise<void> {
const fullPath = path.join(this.basePath, relativePath)
await fs.mkdir(fullPath, { recursive: true })
}
async createFile (relativePath: string, mode: number, size: number): Promise<FileDownload> {
const fullPath = path.join(this.basePath, relativePath)
await fs.mkdir(path.dirname(fullPath), { recursive: true })
const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron)
await wrapPromise(this.zone, fileDownload.open())
return fileDownload
}
close (): void {
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
}
}

View File

@ -1,7 +1,7 @@
import * as fs from 'fs/promises'
import * as fsSync from 'fs'
import * as path from 'path'
import slugify from 'slugify'
import * as glob from 'glob'
import * as yaml from 'js-yaml'
import { Injectable } from '@angular/core'
import { PartialProfile } from 'tabby-core'
@ -14,7 +14,7 @@ import {
} from 'tabby-ssh'
import { ElectronService } from './services/electron.service'
import SSHConfig, { LineType } from 'ssh-config'
import SSHConfig, { Directive, LineType } from 'ssh-config'
// Enum to delineate the properties in SSHProfile options
enum SSHProfilePropertyNames {
@ -90,24 +90,78 @@ function convertSSHConfigValuesToString (arg: string | string[] | object[]): str
.join(' ')
}
// Function to read in the SSH config file and return it as a string
async function readSSHConfigFile (filePath: string): Promise<string> {
try {
return await fs.readFile(filePath, 'utf8')
} catch (err) {
console.error('Error reading SSH config file:', err)
return ''
// Function to read in the SSH config file recursively and parse any Include directives
async function parseSSHConfigFile (
filePath: string,
visited = new Set<string>(),
): Promise<SSHConfig> {
// If we've already processed this file, return an empty config to avoid infinite recursion
if (visited.has(filePath)) {
return SSHConfig.parse('')
}
visited.add(filePath)
let raw = ''
try {
raw = await fs.readFile(filePath, 'utf8')
} catch (err) {
console.error(`Error reading SSH config file: ${filePath}`, err)
return SSHConfig.parse('')
}
const parsed = SSHConfig.parse(raw)
const merged = SSHConfig.parse('')
for (const entry of parsed) {
if (entry.type === LineType.DIRECTIVE && entry.param.toLowerCase() === 'include') {
const directive = entry as Directive
if (typeof directive.value !== 'string') {
continue
}
// ssh_config(5) says "Files without absolute paths are assumed to be in ~/.ssh if included in a user configuration file or /etc/ssh if included from the system configuration file."
let incPath = ''
if (path.isAbsolute(directive.value)) {
incPath = directive.value
} else if (directive.value.startsWith('~')) {
incPath = path.join(process.env.HOME ?? '~', directive.value.slice(1))
} else {
incPath = path.join(process.env.HOME ?? '~', '.ssh', directive.value)
}
const matches = glob.sync(incPath)
for (const match of matches) {
const stat = await fs.stat(match)
if (stat.isDirectory()) {
continue
}
const matchedConfig = await parseSSHConfigFile(match, visited)
merged.push(...matchedConfig)
}
} else {
merged.push(entry)
}
}
return merged
}
// Function to convert an SSH Profile name into a sha256 hash-based ID
async function hashSSHProfileName (name: string) {
const textEncoder = new TextEncoder()
const encoded = textEncoder.encode(name)
const hash = await crypto.subtle.digest('SHA-256', encoded)
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Function to take an ssh-config entry and convert it into an SSHProfile
function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): PartialProfile<SSHProfile> {
async function convertHostToSSHProfile (host: string, settings: Record<string, string | string[] | object[] >): Promise<PartialProfile<SSHProfile>> {
// inline function to generate an id for this profile
const deriveID = (name: string) => 'openssh-config:' + slugify(name)
const deriveID = async (name: string) => 'openssh-config:' + await hashSSHProfileName(name)
// Start point of the profile, with an ID, name, type and group
const thisProfile: PartialProfile<SSHProfile> = {
id: deriveID(host),
id: await deriveID(host),
name: `${host} (.ssh/config)`,
type: 'ssh',
group: 'Imported from .ssh/config',
@ -148,7 +202,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
const basicString = settings[key]
if (typeof basicString === 'string') {
if (targetName === SSHProfilePropertyNames.JumpHost) {
options[targetName] = deriveID(basicString)
options[targetName] = await deriveID(basicString)
} else {
options[targetName] = basicString
}
@ -159,9 +213,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
// The following have single integer values
case SSHProfilePropertyNames.Port:
case SSHProfilePropertyNames.KeepaliveInterval:
case SSHProfilePropertyNames.KeepaliveCountMax:
case SSHProfilePropertyNames.ReadyTimeout:
const numberString = settings[key]
if (typeof numberString === 'string') {
options[targetName] = parseInt(numberString, 10)
@ -170,6 +222,22 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
}
break
// KeepaliveInterval and ReadyTimeout are in seconds in SSH config but milliseconds in Tabby
case SSHProfilePropertyNames.KeepaliveInterval:
case SSHProfilePropertyNames.ReadyTimeout:
const secondsString = settings[key]
if (typeof secondsString === 'string') {
const parsedSeconds = parseInt(secondsString, 10)
if (!isNaN(parsedSeconds) && parsedSeconds >= 0) {
options[targetName] = parsedSeconds * 1000
} else {
console.log(`Invalid value for ${key}: "${secondsString}"`)
}
} else {
console.log('Unexpected value in settings for ' + key)
}
break
// The following have single yes/no values
case SSHProfilePropertyNames.X11:
case SSHProfilePropertyNames.AgentForward:
@ -235,7 +303,7 @@ function convertHostToSSHProfile (host: string, settings: Record<string, string
return thisProfile
}
function convertToSSHProfiles (config: SSHConfig): PartialProfile<SSHProfile>[] {
async function convertToSSHProfiles (config: SSHConfig): Promise<PartialProfile<SSHProfile>[]> {
const myMap = new Map<string, PartialProfile<SSHProfile>>()
function noWildCardsInName (name: string) {
@ -272,8 +340,8 @@ function convertToSSHProfiles (config: SSHConfig): PartialProfile<SSHProfile>[]
if (!(host in myMap)) {
// NOTE: SSHConfig.compute() lies about the return types
const configuration: Record<string, string | string[] | object[]> = config.compute(host)
if (configuration['HostName']) {
myMap[host] = convertHostToSSHProfile(host, configuration)
if (Object.keys(configuration).map(key => key.toLowerCase()).includes('hostname')) {
myMap[host] = await convertHostToSSHProfile(host, configuration)
}
}
}
@ -293,9 +361,8 @@ export class OpenSSHImporter extends SSHProfileImporter {
const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
try {
const sshConfigContent = await readSSHConfigFile(configPath)
const config: SSHConfig = SSHConfig.parse(sshConfigContent)
return convertToSSHProfiles(config)
const config: SSHConfig = await parseSSHConfigFile(configPath)
return await convertToSSHProfiles(config)
} catch (e) {
if (e.code === 'ENOENT') {
return []
@ -317,7 +384,7 @@ export class StaticFileImporter extends SSHProfileImporter {
}
async getProfiles (): Promise<PartialProfile<SSHProfile>[]> {
const deriveID = name => 'file-config:' + slugify(name)
const deriveID = async name => 'file-config:' + await hashSSHProfileName(name)
if (!fsSync.existsSync(this.configPath)) {
return []
@ -328,11 +395,11 @@ export class StaticFileImporter extends SSHProfileImporter {
return []
}
return (yaml.load(content) as PartialProfile<SSHProfile>[]).map(item => ({
return Promise.all((yaml.load(content) as PartialProfile<SSHProfile>[]).map(async item => ({
...item,
id: deriveID(item.name),
id: await deriveID(item.name),
type: 'ssh',
}))
})))
}
}

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@xterm/addon-web-links": "^0.10.0",

View File

@ -13,4 +13,12 @@ export abstract class LinkHandler {
}
abstract handle (uri: string, tab?: BaseTerminalTabComponent<any>): void
private _fullMatchRegex: RegExp | null = null
get fullMatchRegex (): RegExp {
if (!this._fullMatchRegex) {
this._fullMatchRegex = new RegExp(`^${this.regex.source}$`)
}
return this._fullMatchRegex
}
}

View File

@ -31,7 +31,7 @@ export class LinkHighlighterDecorator extends TerminalDecorator {
const openLink = async uri => {
for (const handler of this.handlers) {
if (!handler.regex.test(uri)) {
if (!handler.fullMatchRegex.test(uri)) {
continue
}
if (!await handler.verify(await handler.convert(uri, tab), tab)) {

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"ansi-colors": "^4.1.1",

View File

@ -28,6 +28,10 @@ export class LocalProfileSettingsComponent implements ProfileSettingsComponent<L
// return
// }
this.profile.options.cwd = await this.platform.pickDirectory()
const cwd = await this.platform.pickDirectory()
if (!cwd) {
return
}
this.profile.options.cwd = cwd
}
}

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@types/semver": "^7.1.0",

View File

@ -100,6 +100,7 @@ export class PluginsSettingsTabComponent {
this.busy.delete(plugin.name)
this.config.requestRestart()
} catch (err) {
console.error('Error installing plugin', plugin.name, err)
this.erroredPlugin = plugin.name
this.errorMessage = err
this.busy.delete(plugin.name)
@ -114,6 +115,7 @@ export class PluginsSettingsTabComponent {
this.busy.delete(plugin.name)
this.config.requestRestart()
} catch (err) {
console.error('Error uninstalling plugin', plugin.name, err)
this.erroredPlugin = plugin.name
this.errorMessage = err
this.busy.delete(plugin.name)

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@types/node": "14.14.14",

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@types/marked": "^5.0.1",

View File

@ -67,7 +67,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
div {{cfg.name}}
small.text-muted(
translate='Modified on {date}',
[translateParams]='{date: cfg.modified_at|date:"medium"}'
[translateParams]='{date: cfg.modified_at|tabbyDate}'
)
.me-auto
button.btn.btn-link.ms-1(

View File

@ -145,4 +145,5 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
openTabbyWebInfo () {
this.platform.openExternal('https://github.com/Eugeny/tabby-web')
}
}

View File

@ -8,5 +8,5 @@
)
div(*ngFor='let release of releases')
h1 {{release.name}}
.text-muted {{release.version}} / {{release.date|date:'mediumDate'}}
.text-muted {{release.version}} / {{release.date|tabbyDate}}
section([fastHtmlBind]='release.content')

View File

@ -45,4 +45,5 @@ export class ReleaseNotesComponent extends BaseTabComponent {
onScrolled () {
this.loadReleases(this.lastPage + 1)
}
}

View File

@ -17,7 +17,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@types/node": "20.3.1",

View File

@ -6,10 +6,12 @@ export const supportedAlgorithms = {
[SSHAlgorithmType.HOSTKEY]: russh.getSupportedKeyTypes().filter(x => x !== 'none'),
[SSHAlgorithmType.CIPHER]: russh.getSupportedCiphers().filter(x => x !== 'clear'),
[SSHAlgorithmType.HMAC]: russh.getSupportedMACs().filter(x => x !== 'none'),
[SSHAlgorithmType.COMPRESSION]: russh.getSupportedCompressionAlgorithms().reverse(),
}
export const defaultAlgorithms = {
[SSHAlgorithmType.KEX]: [
'mlkem768x25519-sha256',
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'diffie-hellman-group16-sha512',
@ -42,4 +44,7 @@ export const defaultAlgorithms = {
'hmac-sha1-etm@openssh.com',
'hmac-sha1',
],
[SSHAlgorithmType.COMPRESSION]: [
'none',
],
}

View File

@ -5,6 +5,8 @@ export enum SSHAlgorithmType {
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey',
COMPRESSION = 'compression',
}
export interface SSHProfile extends ConnectableTerminalProfile {

View File

@ -17,6 +17,10 @@
.breadcrumb-spacer.flex-grow-1.h-100((dblclick)='editPath()')
button.btn.btn-link.btn-sm.flex-shrink-0.d-flex(*ngIf='!showFilter', (click)='showFilter = true')
i.fas.fa-filter.me-1
div(translate) Filter
button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='openCreateDirectoryModal()')
i.fas.fa-plus.me-1
div(translate) Create directory
@ -31,6 +35,19 @@
button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')}
.filter-bar.px-3.py-2.border-bottom(*ngIf='showFilter')
.input-group
input.form-control(
type='text',
placeholder='Filter...',
autofocus,
[(ngModel)]='filterText',
(input)='onFilterChange()',
(keydown.escape)='clearFilter()'
)
button.btn.btn-secondary((click)='clearFilter()')
i.fas.fa-times
.body(dropZone, (transfer)='uploadOneFolder($event)')
a.alert.alert-info.d-flex.align-items-center(
*ngIf='shouldShowCWDTip && !cwdDetectionAvailable',
@ -47,13 +64,13 @@
div(*ngIf='fileList === null', translate) Loading
.list-group.list-group-light(*ngIf='fileList !== null')
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngIf='path !== "/"',
*ngIf='path !== "/" && (!showFilter || filterText.trim() === "")',
(click)='goUp()'
)
i.fas.fa-fw.fa-level-up-alt
div(translate) Go up
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let item of fileList',
*ngFor='let item of filteredFileList',
(contextmenu)='showContextMenu(item, $event)',
(click)='open(item)'
)
@ -61,5 +78,8 @@
div {{item.name}}
.me-auto
.size(*ngIf='!item.isDirectory') {{item.size|filesize}}
.date {{item.modified|date:'medium'}}
.date {{item.modified|tabbyDate}}
.mode {{getModeString(item)}}
.alert.alert-info.text-center.mt-3(*ngIf='fileList !== null && filteredFileList.length === 0 && showFilter && filterText.trim() !== ""')
i.fas.fa-search.me-2
span(translate) No files match the filter "{{filterText}}"

View File

@ -9,6 +9,10 @@
flex: none;
}
> .filter-bar {
flex: none;
}
> .body {
padding: 10px 20px;
flex: 1 1 0;

View File

@ -1,7 +1,7 @@
import * as C from 'constants'
import { posix as path } from 'path'
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
import { FileUpload, DirectoryUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
import { FileUpload, DirectoryUpload, DirectoryDownload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
import { SFTPSession, SFTPFile } from '../session/sftp'
import { SSHSession } from '../session/ssh'
import { SFTPContextMenuItemProvider } from '../api'
@ -23,11 +23,14 @@ export class SFTPPanelComponent {
@Output() closed = new EventEmitter<void>()
sftp: SFTPSession
fileList: SFTPFile[]|null = null
filteredFileList: SFTPFile[] = []
@Input() path = '/'
@Output() pathChange = new EventEmitter<string>()
pathSegments: PathSegment[] = []
@Input() cwdDetectionAvailable = false
editingPath: string|null = null
showFilter = false
filterText = ''
constructor (
private ngbModal: NgbModal,
@ -54,6 +57,8 @@ export class SFTPPanelComponent {
this.path = newPath
this.pathChange.next(this.path)
this.clearFilter()
let p = newPath
this.pathSegments = []
while (p !== '/') {
@ -65,6 +70,7 @@ export class SFTPPanelComponent {
}
this.fileList = null
this.filteredFileList = []
try {
this.fileList = await this.sftp.readdir(this.path)
} catch (error) {
@ -79,6 +85,8 @@ export class SFTPPanelComponent {
this.fileList.sort((a, b) =>
dirKey(b) - dirKey(a) ||
a.name.localeCompare(b.name))
this.updateFilteredList()
}
getFileType (fileExtension: string): string {
@ -220,6 +228,68 @@ export class SFTPPanelComponent {
this.sftp.download(itemPath, transfer)
}
async downloadFolder (folder: SFTPFile): Promise<void> {
try {
const transfer = await this.platform.startDownloadDirectory(folder.name, 0)
if (!transfer) {
return
}
// Start background size calculation and download simultaneously
const sizeCalculationPromise = this.calculateFolderSizeAndUpdate(folder, transfer)
const downloadPromise = this.downloadFolderRecursive(folder, transfer, '')
try {
await Promise.all([sizeCalculationPromise, downloadPromise])
transfer.setStatus('')
transfer.setCompleted(true)
} catch (error) {
transfer.cancel()
throw error
} finally {
transfer.close()
}
} catch (error) {
this.notifications.error(`Failed to download folder: ${error.message}`)
throw error
}
}
private async calculateFolderSizeAndUpdate (folder: SFTPFile, transfer: DirectoryDownload) {
let totalSize = 0
const items = await this.sftp.readdir(folder.fullPath)
for (const item of items) {
if (item.isDirectory) {
totalSize += await this.calculateFolderSizeAndUpdate(item, transfer)
} else {
totalSize += item.size
}
transfer.setTotalSize(totalSize)
}
return totalSize
}
private async downloadFolderRecursive (folder: SFTPFile, transfer: DirectoryDownload, relativePath: string): Promise<void> {
const items = await this.sftp.readdir(folder.fullPath)
for (const item of items) {
if (transfer.isCancelled()) {
throw new Error('Download cancelled')
}
const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
transfer.setStatus(itemRelativePath)
if (item.isDirectory) {
await transfer.createDirectory(itemRelativePath)
await this.downloadFolderRecursive(item, transfer, itemRelativePath)
} else {
const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size)
await this.sftp.download(item.fullPath, fileDownload)
}
}
}
getModeString (item: SFTPFile): string {
const s = 'SGdrwxrwxrwx'
const e = ' ---------'
@ -273,4 +343,30 @@ export class SFTPPanelComponent {
close (): void {
this.closed.emit()
}
clearFilter (): void {
this.showFilter = false
this.filterText = ''
this.updateFilteredList()
}
onFilterChange (): void {
this.updateFilteredList()
}
private updateFilteredList (): void {
if (!this.fileList) {
this.filteredFileList = []
return
}
if (!this.showFilter || this.filterText.trim() === '') {
this.filteredFileList = this.fileList
return
}
this.filteredFileList = this.fileList.filter(item =>
item.name.toLowerCase().includes(this.filterText.toLowerCase()),
)
}
}

View File

@ -285,7 +285,13 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.w-75
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
.form-line.align-items-start
.header
.title Compression
.w-75
div(*ngFor='let alg of supportedAlgorithms.compression')
checkbox([text]='alg', [(ngModel)]='algorithms.compression[alg]')
li(ngbNavItem)
a(ngbNavLink, translate) Colors
ng-template(ngbNavContent)

View File

@ -107,7 +107,7 @@ export class SSHProfileSettingsComponent {
this.profile.options.algorithms![k] = Object.entries(this.algorithms[k])
.filter(([_, v]) => !!v)
.map(([key, _]) => key)
this.profile.options.algorithms![k].sort()
if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms![k].sort() }
}
if (this.connectionMode !== 'jumpHost') {

View File

@ -179,6 +179,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
try {
await this.initializeSessionMaybeMultiplex(false)
} catch (e) {
console.error('SSH session initialization failed', e)
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
return
}

View File

@ -66,4 +66,5 @@ export default class SSHModule { }
export * from './api'
export { SFTPFile, SFTPSession } from './session/sftp'
export { SFTPPanelComponent }
export { SFTPPanelComponent, SSHTabComponent }
export { PasswordStorageService } from './services/passwordStorage.service'

View File

@ -33,6 +33,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
kex: [] as string[],
cipher: [] as string[],
serverHostKey: [] as string[],
compression: [] as string[],
},
proxyCommand: null,
forwardedPorts: [],
@ -55,7 +56,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
super()
for (const k of Object.values(SSHAlgorithmType)) {
this.configDefaults.options.algorithms[k] = [...defaultAlgorithms[k]]
this.configDefaults.options.algorithms[k].sort()
if (k !== SSHAlgorithmType.COMPRESSION) { this.configDefaults.options.algorithms[k].sort() }
}
}

View File

@ -10,33 +10,45 @@ export const VAULT_SECRET_TYPE_PASSPHRASE = 'ssh:key-passphrase'
export class PasswordStorageService {
constructor (private vault: VaultService) { }
async savePassword (profile: SSHProfile, password: string): Promise<void> {
async savePassword (profile: SSHProfile, password: string, username?: string): Promise<void> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
this.vault.addSecret({ type: VAULT_SECRET_TYPE_PASSWORD, key, value: password })
} else {
if (!account) {
return
}
const key = this.getKeytarKeyForConnection(profile)
return keytar.setPassword(key, profile.options.user, password)
return keytar.setPassword(key, account, password)
}
}
async deletePassword (profile: SSHProfile): Promise<void> {
async deletePassword (profile: SSHProfile, username?: string): Promise<void> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
this.vault.removeSecret(VAULT_SECRET_TYPE_PASSWORD, key)
} else {
if (!account) {
return
}
const key = this.getKeytarKeyForConnection(profile)
await keytar.deletePassword(key, profile.options.user)
await keytar.deletePassword(key, account)
}
}
async loadPassword (profile: SSHProfile): Promise<string|null> {
async loadPassword (profile: SSHProfile, username?: string): Promise<string|null> {
const account = username ?? profile.options.user
if (this.vault.isEnabled()) {
const key = this.getVaultKeyForConnection(profile)
const key = this.getVaultKeyForConnection(profile, account)
return (await this.vault.getSecret(VAULT_SECRET_TYPE_PASSWORD, key))?.value ?? null
} else {
if (!account) {
return null
}
const key = this.getKeytarKeyForConnection(profile)
return keytar.getPassword(key, profile.options.user)
return keytar.getPassword(key, account)
}
}
@ -82,9 +94,9 @@ export class PasswordStorageService {
return `ssh-private-key:${id}`
}
private getVaultKeyForConnection (profile: SSHProfile) {
private getVaultKeyForConnection (profile: SSHProfile, username?: string) {
return {
user: profile.options.user,
user: username ?? profile.options.user,
host: profile.options.host,
port: profile.options.port,
}

View File

@ -1,7 +1,8 @@
// import * as fs from 'fs/promises'
import * as fs from 'fs/promises'
import * as crypto from 'crypto'
import * as tmp from 'tmp-promise'
import { Injectable } from '@angular/core'
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
import { ConfigService, FileProvidersService, HostAppService, Platform, PlatformService } from 'tabby-core'
import { SSHSession } from '../session/ssh'
import { SSHProfile } from '../api'
import { PasswordStorageService } from './passwordStorage.service'
@ -15,6 +16,7 @@ export class SSHService {
private config: ConfigService,
hostApp: HostAppService,
private platform: PlatformService,
private fileProviders: FileProvidersService,
) {
if (hostApp.platform === Platform.Windows) {
this.detectedWinSCPPath = platform.getWinSCPPath()
@ -25,18 +27,89 @@ export class SSHService {
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
}
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
async generateWinSCPXTunnelURI (jumpHostProfile: SSHProfile|null): Promise<{ uri: string|null, privateKeyFile?: tmp.FileResult|null }> {
let uri = ''
let tmpFile: tmp.FileResult|null = null
if (jumpHostProfile) {
uri += ';x-tunnel=1'
const jumpHostname = jumpHostProfile.options.host
uri += `;x-tunnelhostname=${jumpHostname}`
const jumpPort = jumpHostProfile.options.port ?? 22
uri += `;x-tunnelportnumber=${jumpPort}`
const jumpUsername = jumpHostProfile.options.user
uri += `;x-tunnelusername=${jumpUsername}`
if (jumpHostProfile.options.auth === 'password') {
const jumpPassword = await this.passwordStorage.loadPassword(jumpHostProfile, jumpUsername)
if (jumpPassword) {
uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}`
}
}
if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys && jumpHostProfile.options.privateKeys.length > 0) {
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(jumpHostProfile)
tmpFile = privateKeyPairs.privateKeyFile
if (tmpFile) {
uri += `;x-tunnelpublickeyfile=${encodeURIComponent(tmpFile.path)}`
}
if (privateKeyPairs.passphrase != null) {
uri += `;x-tunnelpassphraseplain=${encodeURIComponent(privateKeyPairs.passphrase)}`
}
}
}
return { uri: uri, privateKeyFile: tmpFile?? null }
}
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<{ uri: string, privateKeyFile?: tmp.FileResult|null }> {
let uri = `scp://${username ?? profile.options.user}`
const password = await this.passwordStorage.loadPassword(profile)
const password = await this.passwordStorage.loadPassword(profile, username)
if (password) {
uri += ':' + encodeURIComponent(password)
}
let tmpFile: tmp.FileResult|null = null
if (profile.options.jumpHost) {
const jumpHostProfile = this.config.store.profiles.find(x => x.id === profile.options.jumpHost) ?? null
const xTunnelParams = await this.generateWinSCPXTunnelURI(jumpHostProfile)
uri += xTunnelParams.uri ?? ''
tmpFile = xTunnelParams.privateKeyFile ?? null
}
if (profile.options.host.includes(':')) {
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
}else {
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
}
return uri
return { uri, privateKeyFile: tmpFile?? null }
}
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
if (!profile.options.privateKeys || profile.options.privateKeys.length === 0) {
throw new Error('No private keys in profile')
}
const path = this.getWinSCPPath()
if (!path) {
throw new Error('WinSCP not found')
}
let tmpPrivateKeyFile: tmp.FileResult|null = null
let passphrase: string|null = null
const tmpFile: tmp.FileResult = await tmp.file()
for (const pk of profile.options.privateKeys) {
let privateKeyContent: string|null = null
const buffer = await this.fileProviders.retrieveFile(pk)
privateKeyContent = buffer.toString()
await fs.writeFile(tmpFile.path, privateKeyContent)
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
const curPassphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
const winSCPcom = path.slice(0, -3) + 'com'
try {
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', curPassphrase])
} catch (error) {
console.warn('Could not convert private key ', error)
continue
}
tmpPrivateKeyFile = tmpFile
passphrase = curPassphrase
break
}
return { passphrase, privateKeyFile: tmpPrivateKeyFile }
}
async launchWinSCP (session: SSHSession): Promise<void> {
@ -44,17 +117,26 @@ export class SSHService {
if (!path) {
return
}
const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)
const args = [winscpParms.uri]
let tmpFile: tmp.FileResult|null = null
if (session.activePrivateKey) {
tmpFile = await tmp.file()
// await fs.writeFile(tmpFile.path, session.activePrivateKey)
const winSCPcom = path.slice(0, -3) + 'com'
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
args.push(`/privatekey=${tmpFile.path}`)
try {
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
const profile = session.profile
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
tmpFile = privateKeyPairs.privateKeyFile
if (tmpFile) {
args.push(`/privatekey=${tmpFile.path}`)
}
if (privateKeyPairs.passphrase != null) {
args.push(`/passphrase=${privateKeyPairs.passphrase}`)
}
}
await this.platform.exec(path, args)
} finally {
tmpFile?.cleanup()
winscpParms.privateKeyFile?.cleanup()
}
await this.platform.exec(path, args)
tmpFile?.cleanup()
}
}

View File

@ -50,6 +50,18 @@ type AuthMethod = {
kind: 'pageant',
}
function sshAuthTypeForMethod (m: AuthMethod): string {
switch (m.type) {
case 'none': return 'none'
case 'hostbased': return 'hostbased'
case 'prompt-password': return 'password'
case 'saved-password': return 'password'
case 'keyboard-interactive': return 'keyboard-interactive'
case 'publickey': return 'publickey'
case 'agent': return 'publickey'
}
}
export class KeyboardInteractivePrompt {
readonly responses: string[] = []
@ -86,7 +98,7 @@ export class SSHSession {
ssh: russh.SSHClient|russh.AuthenticatedSSHClient
sftp?: russh.SFTP
forwardedPorts: ForwardedPort[] = []
jumpChannel: russh.Channel|null = null
jumpChannel: russh.NewChannel|null = null
savedPassword?: string
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
@ -99,7 +111,7 @@ export class SSHSession {
private logger: Logger
private refCount = 0
private remainingAuthMethods: AuthMethod[] = []
private allAuthMethods: AuthMethod[] = []
private serviceMessage = new Subject<string>()
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
private willDestroy = new Subject<void>()
@ -113,6 +125,7 @@ export class SSHSession {
private translate: TranslateService
private knownHosts: SSHKnownHostsService
private privateKeyImporters: AutoPrivateKeyLocator[]
private previouslyDisconnected = false
constructor (
private injector: Injector,
@ -137,29 +150,36 @@ export class SSHSession {
})
}
private addPublicKeyAuthMethod (name: string, contents: Buffer) {
this.allAuthMethods.push({
type: 'publickey',
name,
contents,
})
}
async init (): Promise<void> {
this.remainingAuthMethods = [{ type: 'none' }]
this.allAuthMethods = [{ type: 'none' }]
if (!this.profile.options.auth || this.profile.options.auth === 'publicKey') {
if (this.profile.options.privateKeys?.length) {
for (const pk of this.profile.options.privateKeys) {
for (let pk of this.profile.options.privateKeys) {
// eslint-disable-next-line @typescript-eslint/init-declarations
let contents: Buffer
pk = pk.replace('%h', this.profile.options.host)
pk = pk.replace('%r', this.profile.options.user)
try {
this.remainingAuthMethods.push({
type: 'publickey',
name: pk,
contents: await this.fileProviders.retrieveFile(pk),
})
contents = await this.fileProviders.retrieveFile(pk)
} catch (error) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Could not load private key ${pk}: ${error}`)
continue
}
this.addPublicKeyAuthMethod(pk, contents)
}
} else {
for (const importer of this.privateKeyImporters) {
for (const [name, contents] of await importer.getKeys()) {
this.remainingAuthMethods.push({
type: 'publickey',
name,
contents,
})
this.addPublicKeyAuthMethod(name, contents)
}
}
}
@ -170,7 +190,7 @@ export class SSHSession {
if (!spec) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running Agent process is found`)
} else {
this.remainingAuthMethods.push({
this.allAuthMethods.push({
type: 'agent',
...spec,
})
@ -178,22 +198,51 @@ export class SSHSession {
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
if (this.profile.options.password) {
this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
this.allAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
}
const password = await this.passwordStorage.loadPassword(this.profile)
if (password) {
this.remainingAuthMethods.push({ type: 'saved-password', password })
}
this.remainingAuthMethods.push({ type: 'prompt-password' })
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
if (this.profile.options.password) {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword: this.profile.options.password })
}
this.allAuthMethods.push({ type: 'keyboard-interactive' })
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
this.allAuthMethods.push({ type: 'prompt-password' })
}
this.allAuthMethods.push({ type: 'hostbased' })
}
private async populateStoredPasswordsForResolvedUsername (): Promise<void> {
if (!this.authUsername) {
return
}
const storedPassword = await this.passwordStorage.loadPassword(this.profile, this.authUsername)
if (!storedPassword) {
return
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') {
const hasSavedPassword = this.allAuthMethods.some(method => method.type === 'saved-password' && method.password === storedPassword)
if (!hasSavedPassword) {
const promptIndex = this.allAuthMethods.findIndex(method => method.type === 'prompt-password')
const insertIndex = promptIndex >= 0 ? promptIndex : this.allAuthMethods.length
this.allAuthMethods.splice(insertIndex, 0, { type: 'saved-password', password: storedPassword })
}
}
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const existingSaved = this.allAuthMethods.find(method => method.type === 'keyboard-interactive' && method.savedPassword === storedPassword)
if (!existingSaved) {
const updatable = this.allAuthMethods.find(method => method.type === 'keyboard-interactive' && method.savedPassword === undefined)
if (updatable && updatable.type === 'keyboard-interactive') {
updatable.savedPassword = storedPassword
} else {
this.allAuthMethods.push({ type: 'keyboard-interactive', savedPassword: storedPassword })
}
}
this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
}
this.remainingAuthMethods.push({ type: 'hostbased' })
}
private async getAgentConnectionSpec (): Promise<russh.AgentConnectionSpec|null> {
@ -235,7 +284,7 @@ export class SSHSession {
throw new Error('Cannot open SFTP session before auth')
}
if (!this.sftp) {
this.sftp = await this.ssh.openSFTPChannel()
this.sftp = await this.ssh.activateSFTP(await this.ssh.openSessionChannel())
}
return new SFTPSession(this.sftp, this.injector)
}
@ -256,7 +305,7 @@ export class SSHSession {
const argv = shellQuote.parse(this.profile.options.proxyCommand)
transport = await russh.SshTransport.newCommand(argv[0], argv.slice(1))
} else if (this.jumpChannel) {
transport = await russh.SshTransport.newSshChannel(await this.jumpChannel.take())
transport = await russh.SshTransport.newSshChannel(this.jumpChannel.take())
this.jumpChannel = null
} else if (this.profile.options.socksProxyHost) {
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
@ -293,6 +342,7 @@ export class SSHSession {
kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
compression: this.profile.options.algorithms?.[SSHAlgorithmType.COMPRESSION]?.filter(x => supportedAlgorithms[SSHAlgorithmType.COMPRESSION].includes(x)),
},
keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000),
keepaliveCountMax: this.profile.options.keepaliveCountMax,
@ -306,9 +356,14 @@ export class SSHSession {
}
})
this.previouslyDisconnected = false
this.ssh.disconnect$.subscribe(() => {
if (this.open) {
this.destroy()
if (!this.previouslyDisconnected) {
this.previouslyDisconnected = true
// Let service messages drain
setTimeout(() => {
this.destroy()
})
}
})
@ -335,12 +390,14 @@ export class SSHSession {
}
}
await this.populateStoredPasswordsForResolvedUsername()
const authenticatedClient = await this.handleAuth()
if (authenticatedClient) {
this.ssh = authenticatedClient
} else {
this.ssh.disconnect()
this.passwordStorage.deletePassword(this.profile)
this.passwordStorage.deletePassword(this.profile, this.authUsername ?? undefined)
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error('Authentication rejected')
}
@ -348,7 +405,7 @@ export class SSHSession {
// auth success
if (this.savedPassword) {
this.passwordStorage.savePassword(this.profile, this.savedPassword)
this.passwordStorage.savePassword(this.profile, this.savedPassword, this.authUsername ?? undefined)
}
for (const fw of this.profile.options.forwardedPorts ?? []) {
@ -359,22 +416,31 @@ export class SSHSession {
this.ssh.tcpChannelOpen$.subscribe(async event => {
this.logger.info(`Incoming forwarded connection: ${event.clientAddress}:${event.clientPort} -> ${event.targetAddress}:${event.targetPort}`)
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot open agent channel before auth')
}
const channel = await this.ssh.activateChannel(event.channel)
const forward = this.forwardedPorts.find(x => x.port === event.targetPort && x.host === event.targetAddress)
if (!forward) {
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${event.targetAddress}:${event.targetPort}`)
channel.close()
return
}
const socket = new Socket()
socket.connect(forward.targetPort, forward.targetAddress)
socket.on('error', e => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
event.channel.close()
channel.close()
})
event.channel.data$.subscribe(data => socket.write(data))
socket.on('data', data => event.channel.write(Uint8Array.from(data)))
event.channel.closed$.subscribe(() => socket.destroy())
socket.on('close', () => event.channel.close())
channel.data$.subscribe(data => socket.write(data))
socket.on('data', data => channel.write(Uint8Array.from(data)))
channel.closed$.subscribe(() => socket.destroy())
socket.on('close', () => channel.close())
socket.on('connect', () => {
this.logger.info('Connection forwarded')
})
@ -385,22 +451,28 @@ export class SSHSession {
const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
this.logger.debug(`Trying display ${displaySpec}`)
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot open agent channel before auth')
}
const channel = await this.ssh.activateChannel(event.channel)
const socket = new X11Socket()
try {
const x11Stream = await socket.connect(displaySpec)
this.logger.info('Connection forwarded')
event.channel.data$.subscribe(data => {
channel.data$.subscribe(data => {
x11Stream.write(data)
})
x11Stream.on('data', data => {
event.channel.write(Uint8Array.from(data))
channel.write(Uint8Array.from(data))
})
event.channel.closed$.subscribe(() => {
channel.closed$.subscribe(() => {
socket.destroy()
})
x11Stream.on('close', () => {
event.channel.close()
channel.close()
})
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
@ -411,11 +483,17 @@ export class SSHSession {
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
}
event.channel.close()
channel.close()
}
})
this.ssh.agentChannelOpen$.subscribe(async channel => {
this.ssh.agentChannelOpen$.subscribe(async newChannel => {
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot open agent channel before auth')
}
const channel = await this.ssh.activateChannel(newChannel)
const spec = await this.getAgentConnectionSpec()
if (!spec) {
await channel.close()
@ -469,7 +547,23 @@ export class SSHSession {
this.keyboardInteractivePrompt.next(prompt)
}
async handleAuth (methodsLeft?: string[] | null): Promise<russh.AuthenticatedSSHClient|null> {
async handleAuth (): Promise<russh.AuthenticatedSSHClient|null> {
const subscription = this.ssh.disconnect$.subscribe(() => {
// Auto auth and >=3 keys found
if (!this.profile.options.auth && this.allAuthMethods.filter(x => x.type === 'publickey').length >= 3) {
this.emitServiceMessage('The server has disconnected during authentication.')
this.emitServiceMessage('This may happen if too many private key authentication attemps are made.')
this.emitServiceMessage('You can set the specific private key for authentication in the profile settings.')
}
})
try {
return await this._handleAuth()
} finally {
subscription.unsubscribe()
}
}
private async _handleAuth (): Promise<russh.AuthenticatedSSHClient|null> {
this.activePrivateKey = null
if (!(this.ssh instanceof russh.SSHClient)) {
@ -480,22 +574,37 @@ export class SSHSession {
throw new Error('No username')
}
const noneResult = await this.ssh.authenticateNone(this.authUsername)
if (noneResult instanceof russh.AuthenticatedSSHClient) {
return noneResult
}
let remainingMethods = [...this.allAuthMethods]
let methodsLeft = noneResult.remainingMethods
function maybeSetRemainingMethods (r: russh.AuthFailure) {
if (r.remainingMethods.length) {
methodsLeft = r.remainingMethods
}
}
while (true) {
const method = this.remainingAuthMethods.shift()
if (!method) {
const m = methodsLeft
const method = remainingMethods.find(x => m.length === 0 || m.includes(sshAuthTypeForMethod(x)))
if (this.previouslyDisconnected || !method) {
return null
}
if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
// Agent can still be used even if not in methodsLeft
this.logger.info('Server does not support auth method', method.type)
continue
}
remainingMethods = remainingMethods.filter(x => x !== method)
if (method.type === 'saved-password') {
this.emitServiceMessage(this.translate.instant('Using saved password'))
const result = await this.ssh.authenticateWithPassword(this.authUsername, method.password)
if (result) {
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
}
if (method.type === 'prompt-password') {
const modal = this.ngbModal.open(PromptModalComponent)
@ -510,9 +619,10 @@ export class SSHSession {
this.savedPassword = promptResult.value
}
const result = await this.ssh.authenticateWithPassword(this.authUsername, promptResult.value)
if (result) {
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
} else {
continue
}
@ -523,10 +633,12 @@ export class SSHSession {
if (method.type === 'publickey') {
try {
const key = await this.loadPrivateKey(method.name, method.contents)
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key)
if (result) {
this.emitServiceMessage(`Trying private key: ${method.name}`)
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key, null)
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
} catch (e) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
continue
@ -537,6 +649,7 @@ export class SSHSession {
while (true) {
if (state.state === 'failure') {
maybeSetRemainingMethods(state)
break
}
@ -572,7 +685,7 @@ export class SSHSession {
}
}
state = await this.ssh .continueKeyboardInteractiveAuthentication(responses)
state = await this.ssh.continueKeyboardInteractiveAuthentication(responses)
if (state instanceof russh.AuthenticatedSSHClient) {
return state
@ -582,9 +695,10 @@ export class SSHSession {
if (method.type === 'agent') {
try {
const result = await this.ssh.authenticateWithAgent(this.authUsername, method)
if (result) {
if (result instanceof russh.AuthenticatedSSHClient) {
return result
}
maybeSetRemainingMethods(result)
} catch (e) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to authenticate using agent: ${e}`)
continue
@ -603,7 +717,7 @@ export class SSHSession {
reject()
return
}
const channel = await this.ssh.openTCPForwardChannel({
const channel = await this.ssh.activateChannel(await this.ssh.openTCPForwardChannel({
addressToConnectTo: targetAddress,
portToConnectTo: targetPort,
originatorAddress: sourceAddress ?? '127.0.0.1',
@ -612,7 +726,7 @@ export class SSHSession {
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
reject()
throw err
})
}))
const socket = accept()
channel.data$.subscribe(data => socket.write(data))
socket.on('data', data => channel.write(Uint8Array.from(data)))
@ -669,7 +783,7 @@ export class SSHSession {
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot open shell channel before auth')
}
const ch = await this.ssh.openSessionChannel()
const ch = await this.ssh.activateChannel(await this.ssh.openSessionChannel())
await ch.requestPTY('xterm-256color', {
columns: 80,
rows: 24,
@ -692,13 +806,15 @@ export class SSHSession {
}
async loadPrivateKey (name: string, privateKeyContents: Buffer): Promise<russh.KeyPair> {
this.emitServiceMessage(`Loading private key: ${name}`)
this.activePrivateKey = await this.loadPrivateKeyWithPassphraseMaybe(privateKeyContents.toString())
return this.activePrivateKey
}
async loadPrivateKeyWithPassphraseMaybe (privateKey: string): Promise<russh.KeyPair> {
const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
privateKey = privateKey.replaceAll('EC PRIVATE KEY', 'PRIVATE KEY')
let triedSavedPassphrase = false
let passphrase: string|null = null
while (true) {
@ -710,7 +826,12 @@ export class SSHSession {
triedSavedPassphrase = true
continue
}
if (e.toString() === 'Error: Keys(KeyIsEncrypted)' || e.toString() === 'Error: Keys(SshKey(Crypto))') {
if ([
'Error: Keys(KeyIsEncrypted)',
'Error: Keys(SshKey(Ppk(Encrypted)))',
'Error: Keys(SshKey(Ppk(IncorrectMac)))',
'Error: Keys(SshKey(Crypto))',
].includes(e.toString())) {
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
const modal = this.ngbModal.open(PromptModalComponent)

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { MenuItemOptions, PlatformService, TranslateService } from 'tabby-core'
import { MenuItemOptions, PlatformService, TranslateService, HostAppService, Platform } from 'tabby-core'
import { SFTPSession, SFTPFile } from './session/sftp'
import { SFTPContextMenuItemProvider } from './api'
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
@ -16,37 +16,49 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
private platform: PlatformService,
private ngbModal: NgbModal,
private translate: TranslateService,
private hostApp: HostAppService,
) {
super()
}
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
return [
const items: MenuItemOptions[] = [
{
click: async () => {
await panel.openCreateDirectoryModal()
},
label: this.translate.instant('Create directory'),
},
{
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: this.translate.instant('Delete {fullPath}?', item),
defaultId: 0,
cancelId: 1,
buttons: [
this.translate.instant('Delete'),
this.translate.instant('Cancel'),
],
})).response === 0) {
await this.deleteItem(item, panel.sftp)
panel.navigate(panel.path)
}
},
label: this.translate.instant('Delete'),
},
]
// Add download folder option for directories (only in electron)
if (item.isDirectory && this.hostApp.platform !== Platform.Web) {
items.push({
click: () => panel.downloadFolder(item),
label: this.translate.instant('Download directory'),
})
}
items.push({
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: this.translate.instant('Delete {fullPath}?', item),
defaultId: 0,
cancelId: 1,
buttons: [
this.translate.instant('Delete'),
this.translate.instant('Cancel'),
],
})).response === 0) {
await this.deleteItem(item, panel.sftp)
panel.navigate(panel.path)
}
},
label: this.translate.instant('Delete'),
})
return items
}
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {

View File

@ -15,7 +15,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"ansi-colors": "^4.1.1",

View File

@ -16,7 +16,7 @@
"dist",
"typings"
],
"author": "Eugene Pankov",
"author": "Tabby Developers",
"license": "MIT",
"devDependencies": {
"@xterm/addon-canvas": "^0.6.0",

View File

@ -17,6 +17,8 @@ import { getTerminalBackgroundColor } from '../helpers'
const INACTIVE_TAB_UNLOAD_DELAY = 1000 * 30
const OSC_FOCUS_IN = Buffer.from('\x1b[I')
const OSC_FOCUS_OUT = Buffer.from('\x1b[O')
/**
* A class to base your custom terminal tabs on
@ -309,10 +311,16 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
case 'scroll-to-top':
this.frontend?.scrollToTop()
break
case 'scroll-up':
case 'scroll-page-up':
this.frontend?.scrollPages(-1)
break
case 'scroll-up':
this.frontend?.scrollLines(-1)
break
case 'scroll-down':
this.frontend?.scrollLines(1)
break
case 'scroll-page-down':
this.frontend?.scrollPages(1)
break
case 'scroll-to-bottom':
@ -488,7 +496,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
data = Buffer.from(data, 'utf-8')
}
this.session?.feedFromTerminal(data)
if (this.config.store.terminal.scrollOnInput) {
if (this.config.store.terminal.scrollOnInput && !data.equals(OSC_FOCUS_IN) && !data.equals(OSC_FOCUS_OUT)) {
this.frontend?.scrollToBottom()
}
}
@ -536,7 +544,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
}
if (!this.alternateScreenActive) {
if (data.includes('\r') && this.config.store.terminal.warnOnMultilinePaste) {
if ((data.includes('\r') || data.includes('\n')) && this.config.store.terminal.warnOnMultilinePaste) {
const buttons = [
this.translate.instant('Paste'),
this.translate.instant('Cancel'),

View File

@ -6,6 +6,8 @@
[class.text-danger]='state.resultCount == 0',
(click)='$event.stopPropagation()',
(keyup.enter)='findPrevious()',
(keyup.up)='findPrevious()',
(keyup.down)='findNext()',
(keyup.esc)='close.emit()',
[placeholder]='"Search"|translate'
)
@ -14,14 +16,14 @@
ng-container(*ngIf='state.resultCount > 0')
button.btn.btn-link(
(click)='findPrevious()',
ngbTooltip='Search up',
[ngbTooltip]='"Search up"|translate',
placement='bottom',
[fastHtmlBind]='icons.arrowUp'
)
button.btn.btn-link(
(click)='findNext()',
ngbTooltip='Search down',
[ngbTooltip]='"Search down"|translate',
placement='bottom',
[fastHtmlBind]='icons.arrowDown'
)
@ -32,7 +34,7 @@ button.btn(
(click)='options.caseSensitive = !options.caseSensitive; saveSearchOptions()',
[class.btn-link]='!options.caseSensitive',
[class.btn-info]='options.caseSensitive',
ngbTooltip='Case sensitivity',
[ngbTooltip]='"Case sensitivity"|translate',
placement='bottom',
[fastHtmlBind]='icons.case'
)
@ -41,7 +43,7 @@ button.btn(
(click)='options.regex = !options.regex; saveSearchOptions()',
[class.btn-link]='!options.regex',
[class.btn-info]='options.regex',
ngbTooltip='Regular expression',
[ngbTooltip]='"Regular expression"|translate',
placement='bottom',
[fastHtmlBind]='icons.regexp'
)
@ -50,7 +52,7 @@ button.btn(
(click)='options.wholeWord = !options.wholeWord; saveSearchOptions()',
[class.btn-link]='!options.wholeWord',
[class.btn-info]='options.wholeWord',
ngbTooltip='Whole word',
[ngbTooltip]='"Whole word"|translate',
placement='bottom',
[fastHtmlBind]='icons.wholeWord'
)

View File

@ -101,8 +101,10 @@ export class TerminalConfigProvider extends ConfigProvider {
'⌘-⌥-Shift-I',
],
'scroll-to-top': ['Shift-PageUp'],
'scroll-up': ['⌥-PageUp'],
'scroll-down': ['⌥-PageDown'],
'scroll-page-up': ['⌥-PageUp'],
'scroll-up': ['Ctrl-Shift-Up'],
'scroll-down': ['Ctrl-Shift-Down'],
'scroll-page-down': ['⌥-PageDown'],
'scroll-to-bottom': ['Shift-PageDown'],
},
},
@ -152,8 +154,10 @@ export class TerminalConfigProvider extends ConfigProvider {
'Ctrl-Alt-Shift-I',
],
'scroll-to-top': ['Ctrl-PageUp'],
'scroll-up': ['Alt-PageUp'],
'scroll-down': ['Alt-PageDown'],
'scroll-page-up': ['Alt-PageUp'],
'scroll-up': ['Ctrl-Shift-Up'],
'scroll-down': ['Ctrl-Shift-Down'],
'scroll-page-down': ['Alt-PageDown'],
'scroll-to-bottom': ['Ctrl-PageDown'],
},
},
@ -201,8 +205,10 @@ export class TerminalConfigProvider extends ConfigProvider {
'Ctrl-Alt-Shift-I',
],
'scroll-to-top': ['Ctrl-PageUp'],
'scroll-up': ['Alt-PageUp'],
'scroll-down': ['Alt-PageDown'],
'scroll-page-up': ['Alt-PageUp'],
'scroll-up': ['Ctrl-Shift-Up'],
'scroll-down': ['Ctrl-Shift-Down'],
'scroll-page-down': ['Alt-PageDown'],
'scroll-to-bottom': ['Ctrl-PageDown'],
},
},

View File

@ -27,7 +27,7 @@ class ZModemMiddleware extends SessionMiddleware {
this.logger = log.create('zmodem')
this.sentry = new ZModem.Sentry({
to_terminal: data => {
if (this.isActive) {
if (this.isActive && this.activeSession) {
this.outputToTerminal.next(Buffer.from(data))
}
},
@ -42,25 +42,32 @@ class ZModemMiddleware extends SessionMiddleware {
},
on_retract: () => {
this.showMessage('transfer cancelled')
this.activeSession = null
this.isActive = false
},
})
}
feedFromSession (data: Buffer): void {
const chunkSize = 1024
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
if (this.isActive || this.activeSession) {
try {
this.sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
this.sentry.consume(data)
} catch (e) {
this.showMessage(colors.bgRed.black(' Error ') + ' ' + e)
this.logger.error('protocol error', e)
this.activeSession.abort()
this.activeSession?.abort()
this.activeSession = null
this.isActive = false
// Don't forward the problematic data to terminal
return
}
}
if (!this.isActive) {
} else {
try {
this.sentry.consume(data)
} catch (e) {
this.logger.error('zmodem detection error', e)
}
this.outputToTerminal.next(data)
}
}
@ -73,25 +80,35 @@ class ZModemMiddleware extends SessionMiddleware {
this.activeSession = zsession
this.logger.info('new session', zsession)
if (zsession.type === 'send') {
const transfers = await this.platform.startUpload({ multiple: true })
let filesRemaining = transfers.length
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
for (const transfer of transfers) {
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
filesRemaining--
sizeRemaining -= transfer.getSize()
try {
if (zsession.type === 'send') {
const transfers = await this.platform.startUpload({ multiple: true })
let filesRemaining = transfers.length
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
for (const transfer of transfers) {
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
filesRemaining--
sizeRemaining -= transfer.getSize()
}
await zsession.close()
} else {
zsession.on('offer', xfer => {
this.receiveFile(xfer, zsession)
})
zsession.start()
await new Promise(resolve => zsession.on('session_end', resolve))
}
this.activeSession = null
await zsession.close()
} else {
zsession.on('offer', xfer => {
this.receiveFile(xfer, zsession)
})
zsession.start()
await new Promise(resolve => zsession.on('session_end', resolve))
this.showMessage(colors.bgBlue.black(' ZMODEM ') + ' Complete')
} catch (error) {
this.logger.error('ZMODEM session error', error)
this.showMessage(colors.bgRed.black(' ZMODEM ') + ` Session failed: ${error.message}`)
try {
zsession.abort()
} catch { }
} finally {
this.activeSession = null
}
}

View File

@ -77,6 +77,7 @@ export abstract class Frontend {
abstract visualBell (): void
abstract scrollToTop (): void
abstract scrollLines (amount: number): void
abstract scrollPages (pages: number): void
abstract scrollToBottom (): void

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