mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-01 14:11:50 +00:00
Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
881d5d599d | ||
![]() |
f0e96b5f8b | ||
![]() |
6fed2cb9c0 | ||
![]() |
8e2ffa1654 | ||
![]() |
c25d4bd768 | ||
![]() |
9f8f2966d9 | ||
![]() |
5c976948dd | ||
![]() |
ff4b137088 | ||
![]() |
e3214e38d3 | ||
![]() |
2fc457dd78 | ||
![]() |
3621ea934c | ||
![]() |
b275dac08a | ||
![]() |
47e1bfc810 | ||
![]() |
37e9ba48b1 | ||
![]() |
a54d537536 | ||
![]() |
0c2b221c06 | ||
![]() |
e3375741af | ||
![]() |
d82a88bcc6 | ||
![]() |
f7cab00e4d | ||
![]() |
7f733b8029 | ||
![]() |
d6291c8af4 | ||
![]() |
563852c024 | ||
![]() |
1941d9b748 | ||
![]() |
3473be99bf | ||
![]() |
10cb6a81c7 | ||
![]() |
35f0d6908a | ||
![]() |
2c2d100c27 | ||
![]() |
ab623a7a91 | ||
![]() |
e2e606602b | ||
![]() |
cd3149b601 | ||
![]() |
50748db958 | ||
![]() |
8049dc7332 | ||
![]() |
f468796877 | ||
![]() |
c093780230 | ||
![]() |
c67e44cc9d | ||
![]() |
db45d0c87a | ||
![]() |
4107a01a01 | ||
![]() |
5e378844a1 | ||
![]() |
623496ff52 | ||
![]() |
5082814023 | ||
![]() |
9836b7aefb | ||
![]() |
cb9681ef41 | ||
![]() |
7527b8ac2d | ||
![]() |
da081ba706 | ||
![]() |
e6063da813 | ||
![]() |
e6b4cb94bd | ||
![]() |
56b843c007 | ||
![]() |
a26b30f0bc | ||
![]() |
4a1d8cdd0d | ||
![]() |
7a0920b87c | ||
![]() |
26199ebb76 | ||
![]() |
e3e01558b2 | ||
![]() |
1852486818 | ||
![]() |
d7ea394a15 | ||
![]() |
c4a1d8aa56 | ||
![]() |
be39591c54 | ||
![]() |
94320265b8 | ||
![]() |
b94a943694 | ||
![]() |
1674ec1ebf | ||
![]() |
3923e46f22 | ||
![]() |
ed71b499b9 | ||
![]() |
924fb90220 | ||
![]() |
00191763cf | ||
![]() |
555a21d648 | ||
![]() |
9d88db83ee | ||
![]() |
2bdecc899d | ||
![]() |
ab8992f0aa | ||
![]() |
e474ad573a | ||
![]() |
4c6227fccf | ||
![]() |
02b7b12ea5 | ||
![]() |
d319a54fee | ||
![]() |
397a93bd6f | ||
![]() |
8d3f4137a1 | ||
![]() |
3a615a070b | ||
![]() |
95a04788e5 | ||
![]() |
60a1a1f21c | ||
![]() |
4ad5627823 | ||
![]() |
5a83621c64 | ||
![]() |
ed6d2fc005 | ||
![]() |
63f05a7388 | ||
![]() |
a687377d16 | ||
![]() |
9dc8f66153 | ||
![]() |
e155174bd7 | ||
![]() |
6c06e24b48 | ||
![]() |
a99fcbb71d | ||
![]() |
95b8b0b4dd | ||
![]() |
c47fe51422 | ||
![]() |
25b3aa5850 | ||
![]() |
ff3feb61bc | ||
![]() |
2cb98d65da |
@@ -451,6 +451,24 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Me1onRind",
|
||||||
|
"name": "zZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/19531270?v=4",
|
||||||
|
"profile": "https://github.com/Me1onRind",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "tainoNZ",
|
||||||
|
"name": "Aaron Davison",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/49261322?v=4",
|
||||||
|
"profile": "https://github.com/tainoNZ",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
|||||||
path: artifact-zip
|
path: artifact-zip
|
||||||
|
|
||||||
Linux-Build:
|
Linux-Build:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-latest
|
||||||
needs: Lint
|
needs: Lint
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install bsdtar zsh
|
sudo apt-get install libarchive-tools zsh
|
||||||
npm i -g yarn@1.19.1
|
npm i -g yarn@1.19.1
|
||||||
cd app
|
cd app
|
||||||
yarn
|
yarn
|
||||||
|
12
.github/workflows/docs.yml
vendored
12
.github/workflows/docs.yml
vendored
@@ -2,7 +2,7 @@ name: Docs
|
|||||||
on: push
|
on: push
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -18,8 +18,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
eval $(ssh-agent -s)
|
|
||||||
ssh-add <(echo "$DOCS_PRIVATE_KEY")
|
|
||||||
yarn cache clean
|
yarn cache clean
|
||||||
cd app
|
cd app
|
||||||
yarn
|
yarn
|
||||||
@@ -28,7 +26,13 @@ jobs:
|
|||||||
yarn
|
yarn
|
||||||
yarn run build:typings
|
yarn run build:typings
|
||||||
yarn run docs
|
yarn run docs
|
||||||
rsync -e "ssh -o StrictHostKeyChecking=no" -arv docs/api/ root@ajenti.org:/srv/terminus-docs/
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCS_PRIVATE_KEY: ${{ secrets.DOCS_PRIVATE_KEY }}
|
DOCS_PRIVATE_KEY: ${{ secrets.DOCS_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||||
|
with:
|
||||||
|
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_TABBY_DOCS }}'
|
||||||
|
channelId: live
|
||||||
|
projectId: tabby-docs
|
||||||
|
@@ -38,14 +38,14 @@
|
|||||||
|
|
||||||
# 목차 <!-- omit in toc -->
|
# 목차 <!-- omit in toc -->
|
||||||
|
|
||||||
- [Tabby는 무엇인가](#about)
|
- [Tabby는 무엇인가](#tabby는-무엇인가)
|
||||||
- [터미널 기능](#terminal)
|
- [터미널 기능](#터미널-기능)
|
||||||
- [SSH 클라이언트](#ssh)
|
- [SSH 클라이언트](#ssh-클라이언트)
|
||||||
- [시리얼 터미널](#serial)
|
- [시리얼 터미널](#시리얼-터미널)
|
||||||
- [포터블](#portable)
|
- [포터블](#포터블)
|
||||||
- [플러그인](#plugins)
|
- [플러그인](#플러그인)
|
||||||
- [테마](#themes)
|
- [테마](#테마)
|
||||||
- [기여](#contributing)
|
- [기여](#기여)
|
||||||
|
|
||||||
<a name="about"></a>
|
<a name="about"></a>
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
|
|
||||||
Pull requests and plugins are welcome!
|
Pull requests and plugins are welcome!
|
||||||
|
|
||||||
프로젝트 배치 방법에 대한 자세한 내용과 매우 간단한 플러그인 개발 튜토리얼은 [HACKING.md](https://github.com/Eugeny/tabby/blob/master/HACKING.md) 및 [API docs](http://ajenti.org/terminus-docs/)를 참조하십시오.
|
프로젝트 배치 방법에 대한 자세한 내용과 매우 간단한 플러그인 개발 튜토리얼은 [HACKING.md](https://github.com/Eugeny/tabby/blob/master/HACKING.md) 및 [API docs](https://docs.tabby.sh/)를 참조하십시오.
|
||||||
|
|
||||||
---
|
---
|
||||||
<a name="contributors"></a>
|
<a name="contributors"></a>
|
||||||
|
@@ -135,7 +135,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
|
|||||||
|
|
||||||
Pull requests and plugins are welcome!
|
Pull requests and plugins are welcome!
|
||||||
|
|
||||||
See [HACKING.md](https://github.com/Eugeny/tabby/blob/master/HACKING.md) and [API docs](http://ajenti.org/terminus-docs/) for information of how the project is laid out, and a very brief plugin development tutorial.
|
See [HACKING.md](https://github.com/Eugeny/tabby/blob/master/HACKING.md) and [API docs](https://docs.tabby.sh/) for information of how the project is laid out, and a very brief plugin development tutorial.
|
||||||
|
|
||||||
---
|
---
|
||||||
<a name="contributors"></a>
|
<a name="contributors"></a>
|
||||||
@@ -209,6 +209,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://www.notion.so/3d45c6bd2cbd4f938873a4bd12e23375"><img src="https://avatars.githubusercontent.com/u/59506394?v=4?s=100" width="100px;" alt=""/><br /><sub><b>장보연</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=BoYeonJang" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://www.notion.so/3d45c6bd2cbd4f938873a4bd12e23375"><img src="https://avatars.githubusercontent.com/u/59506394?v=4?s=100" width="100px;" alt=""/><br /><sub><b>장보연</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=BoYeonJang" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/Me1onRind"><img src="https://avatars.githubusercontent.com/u/19531270?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zZ</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=Me1onRind" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/tainoNZ"><img src="https://avatars.githubusercontent.com/u/49261322?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Aaron Davison</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=tainoNZ" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
|
@@ -116,6 +116,7 @@ export class Window {
|
|||||||
}
|
}
|
||||||
this.window.focus()
|
this.window.focus()
|
||||||
this.window.moveTop()
|
this.window.moveTop()
|
||||||
|
application.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -397,6 +398,18 @@ export class Window {
|
|||||||
}
|
}
|
||||||
this.window.on('move', onBoundsChange)
|
this.window.on('move', onBoundsChange)
|
||||||
this.window.on('resize', onBoundsChange)
|
this.window.on('resize', onBoundsChange)
|
||||||
|
|
||||||
|
ipcMain.on('window-set-traffic-light-position', (_event, x, y) => {
|
||||||
|
this.window.setTrafficLightPosition({ x, y })
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-set-opacity', (_event, opacity) => {
|
||||||
|
this.window.setOpacity(opacity)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-set-progress-bar', (_event, value) => {
|
||||||
|
this.window.setProgressBar(value, { mode: value < 0 ? 'none' : 'normal' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroy () {
|
private destroy () {
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
"watch": "webpack --progress --color --watch"
|
"watch": "webpack --progress --color --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^12.2.0",
|
"@angular/cdk": "^12.2.9",
|
||||||
"@electron/remote": "1.2.0",
|
"@electron/remote": "1.2.0",
|
||||||
"@tabby-gang/node-pty": "^0.11.0-beta.200",
|
"@tabby-gang/node-pty": "^0.11.0-beta.200",
|
||||||
"any-promise": "^1.3.0",
|
"any-promise": "^1.3.0",
|
||||||
@@ -29,23 +29,23 @@
|
|||||||
"mz": "^2.7.0",
|
"mz": "^2.7.0",
|
||||||
"native-process-working-directory": "^1.0.2",
|
"native-process-working-directory": "^1.0.2",
|
||||||
"npm": "6",
|
"npm": "6",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.4.0",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.20",
|
||||||
"v8-compile-cache": "^2.3.0",
|
"v8-compile-cache": "^2.3.0",
|
||||||
"yargs": "^17.1.0"
|
"yargs": "^17.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"macos-native-processlist": "^2.0.0",
|
"macos-native-processlist": "^2.0.0",
|
||||||
"serialport": "^9.2.0",
|
"serialport": "^9.2.1",
|
||||||
"windows-blurbehind": "^1.0.1",
|
"windows-blurbehind": "^1.0.1",
|
||||||
"windows-native-registry": "^3.1.0",
|
"windows-native-registry": "^3.1.0",
|
||||||
"windows-process-tree": "^0.3.0"
|
"windows-process-tree": "^0.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mz": "2.7.4",
|
"@types/mz": "2.7.4",
|
||||||
"@types/node": "16.0.1",
|
"@types/node": "16.0.1",
|
||||||
"ngx-filesize": "^2.0.16",
|
"ngx-filesize": "^2.0.16",
|
||||||
"node-abi": "^3.0.0"
|
"node-abi": "^3.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tabby-community-color-schemes": "*",
|
"tabby-community-color-schemes": "*",
|
||||||
|
@@ -163,6 +163,10 @@ ngb-typeahead-window {
|
|||||||
margin: -7px 0;
|
margin: -7px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-box {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Windows high contrast mode
|
// Windows high contrast mode
|
||||||
@media screen and (forced-colors: active) {
|
@media screen and (forced-colors: active) {
|
||||||
|
@@ -7,7 +7,8 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
animation: 0.5s ease-out fadeIn;
|
animation: 0.5s ease-out fadeIn;
|
||||||
background: radial-gradient(#3a66820a 0%, #000e17 30%, black 100%);
|
background-image: radial-gradient(#3a66820a 0%, #000e17 30%, black 100%);
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
&>div {
|
&>div {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@angular/cdk@^12.2.0":
|
"@angular/cdk@^12.2.9":
|
||||||
version "12.2.0"
|
version "12.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.0.tgz#7c6de53522ef7cf911d86e187f3df2a90e8fee49"
|
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.9.tgz#f39e4d7cdb3568ad8e1d412e3500772e2d4c605c"
|
||||||
integrity sha512-Dts+KIMz6EdzQxaWBFcNwgWAHVPkI5pnOGMidKKVOmjezSUN6mhfBKq8emgsddJMRAqz/1VHMAEaRkp0VoBKiA==
|
integrity sha512-9Wgj69iGAZ4teQqW/zPbVg2RGna+m9i3v0zkWGx/+Uo95rikJCUZBQM4bfeOe+bSJrS77jV5EisBWG7ayNUSzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.2.0"
|
tslib "^2.2.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
"@serialport/binding-abstract" "^9.0.7"
|
"@serialport/binding-abstract" "^9.0.7"
|
||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
|
|
||||||
"@serialport/bindings@^9.2.0":
|
"@serialport/bindings@9.2.1":
|
||||||
version "9.2.0"
|
version "9.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@serialport/bindings/-/bindings-9.2.0.tgz#de6df688d0ff99bdbb86ea6db412562cb2d9ebe7"
|
resolved "https://registry.yarnpkg.com/@serialport/bindings/-/bindings-9.2.1.tgz#5e6b83222821f9b849512abdc8637386a6675355"
|
||||||
integrity sha512-s9EKHDZjLHipHhypxy6pz2XsoI1fPiOGU+X13AIGdQfoe7I6piEyhJ2znNgXMugMe43OxNk0/CmuVMzzcw1lmQ==
|
integrity sha512-e1CvbvkuMptSjCKc/YwIGjEsSod7kGRpS5TciACQMOi2QQTD8XwVPim0izqVCBZko4n4b0dC6sG3EBkTkQIwnw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@serialport/binding-abstract" "^9.0.7"
|
"@serialport/binding-abstract" "^9.0.7"
|
||||||
"@serialport/parser-readline" "^9.0.7"
|
"@serialport/parser-readline" "^9.0.7"
|
||||||
@@ -699,14 +699,7 @@ debug@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debug@^4.0.1, debug@^4.3.1:
|
debug@^4.0.1, debug@^4.3.1, debug@^4.3.2:
|
||||||
version "4.3.1"
|
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz"
|
|
||||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
|
||||||
dependencies:
|
|
||||||
ms "2.1.2"
|
|
||||||
|
|
||||||
debug@^4.3.2:
|
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||||
@@ -2145,10 +2138,10 @@ node-abi@^2.20.0, node-abi@^2.7.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^5.4.1"
|
semver "^5.4.1"
|
||||||
|
|
||||||
node-abi@^3.0.0:
|
node-abi@^3.2.0:
|
||||||
version "3.0.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.0.0.tgz#aaeec41ffa8dd436de7a97345ff6f5c99eeafeec"
|
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.2.0.tgz#c8ec6874f808b4da5fbd56e9506390ce65b152a2"
|
||||||
integrity sha512-bAfE5Pp+qqHiz4GkpH64HqHUgK2DippKB3QuYbsOp8QoR8c7S646jJMfsOj+WHZO5dPssO3j+54LwG3w3HeYWg==
|
integrity sha512-/qb92JAb2uiwEQ4aXpVphXfGJU77qdCieXACDaIofcMz+YMPBmnCo8v0OlzJBuXh5QHmMiiI/GKyiCzbjOMn2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
@@ -3053,10 +3046,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
aproba "^1.1.1"
|
aproba "^1.1.1"
|
||||||
|
|
||||||
rxjs@^7.2.0:
|
rxjs@^7.4.0:
|
||||||
version "7.2.0"
|
version "7.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.2.0.tgz#5cd12409639e9514a71c9f5f9192b2c4ae94de31"
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68"
|
||||||
integrity sha512-aX8w9OpKrQmiPKfT1bqETtUr9JygIz6GZ+gql8v7CijClsP0laoFUdKzxFAoWuRdSlOdU2+crss+cMf+cqMTnw==
|
integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "~2.1.0"
|
tslib "~2.1.0"
|
||||||
|
|
||||||
@@ -3106,13 +3099,13 @@ serialize-error@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.8.0"
|
type-fest "^0.8.0"
|
||||||
|
|
||||||
serialport@^9.2.0:
|
serialport@^9.2.1:
|
||||||
version "9.2.0"
|
version "9.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/serialport/-/serialport-9.2.0.tgz#17a8364979f3c06a54a7bf4e8cbb8ebc91e54511"
|
resolved "https://registry.yarnpkg.com/serialport/-/serialport-9.2.1.tgz#9da02f6657ee9e2c8bd7642ff6c5c59a6a65d17e"
|
||||||
integrity sha512-C6AQ4jD4mre3tn3QA+atn++mEZDh4r40CIeh1sKhskKE+Q4eiIr/nzVMOiPxHb8gskrSNxujH+Br49tl3i9s9g==
|
integrity sha512-zX18SVSNRZvMrkxNvSnhqqag46MLQH517EaLSVv8PKSVnTFMzemRCfBdXbOY2XhtUqzpQq6ep2mR1t+AMhJssg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@serialport/binding-mock" "9.0.7"
|
"@serialport/binding-mock" "9.0.7"
|
||||||
"@serialport/bindings" "^9.2.0"
|
"@serialport/bindings" "9.2.1"
|
||||||
"@serialport/parser-byte-length" "9.0.7"
|
"@serialport/parser-byte-length" "9.0.7"
|
||||||
"@serialport/parser-cctalk" "9.0.7"
|
"@serialport/parser-cctalk" "9.0.7"
|
||||||
"@serialport/parser-delimiter" "9.0.7"
|
"@serialport/parser-delimiter" "9.0.7"
|
||||||
@@ -3205,10 +3198,10 @@ sorted-union-stream@~2.1.3:
|
|||||||
from2 "^1.3.0"
|
from2 "^1.3.0"
|
||||||
stream-iterate "^1.1.0"
|
stream-iterate "^1.1.0"
|
||||||
|
|
||||||
source-map-support@^0.5.19:
|
source-map-support@^0.5.20:
|
||||||
version "0.5.19"
|
version "0.5.20"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
|
||||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-from "^1.0.0"
|
buffer-from "^1.0.0"
|
||||||
source-map "^0.6.0"
|
source-map "^0.6.0"
|
||||||
@@ -3715,10 +3708,10 @@ windows-native-registry@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api "^3.1.0"
|
node-addon-api "^3.1.0"
|
||||||
|
|
||||||
windows-process-tree@^0.3.0:
|
windows-process-tree@^0.3.2:
|
||||||
version "0.3.0"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.0.tgz#cf0d9291b22fba2a7f5a687c8272866e28fbcafd"
|
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.2.tgz#8c39f39e7707e09fd74638a7ef644b5f389096d3"
|
||||||
integrity sha512-0bKI4gcd5MOsOpn2TdStCSlnjThtH6BdHrocekY9qCgTqgEtdaUs0B5BaqyzF9jXoTSwz38NMdE1F55o4fgv9Q==
|
integrity sha512-x8Y4KOV8tUhhPiO0TH7wOMTZ677rw7VEwq+dTuHHiLTClkrNXWSY3XzP6ez3fs2Cab4FajrtmiqRs0jTMZHfyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
nan "^2.13.2"
|
nan "^2.13.2"
|
||||||
|
|
||||||
|
@@ -46,7 +46,7 @@ nsis:
|
|||||||
artifactName: tabby-${version}-setup.${ext}
|
artifactName: tabby-${version}-setup.${ext}
|
||||||
installerIcon: "./build/windows/icon.ico"
|
installerIcon: "./build/windows/icon.ico"
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
|
shortcutName: Tabby Terminal
|
||||||
mac:
|
mac:
|
||||||
category: public.app-category.video
|
category: public.app-category.video
|
||||||
icon: "./build/mac/icon.icns"
|
icon: "./build/mac/icon.icns"
|
||||||
|
BIN
extras/UAC.exe
BIN
extras/UAC.exe
Binary file not shown.
10
firebase.json
Normal file
10
firebase.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"hosting": {
|
||||||
|
"public": "docs/api",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
29
package.json
29
package.json
@@ -10,31 +10,31 @@
|
|||||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||||
"@sentry/cli": "^1.67.2",
|
"@sentry/cli": "^1.67.2",
|
||||||
"@sentry/electron": "^2.5.2",
|
"@sentry/electron": "^2.5.4",
|
||||||
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
||||||
"@types/electron-config": "^3.2.2",
|
|
||||||
"@types/deep-equal": "1.0.1",
|
"@types/deep-equal": "1.0.1",
|
||||||
"deep-equal": "2.0.5",
|
"@types/electron-config": "^3.2.2",
|
||||||
"@types/electron-debug": "^2.1.0",
|
"@types/electron-debug": "^2.1.0",
|
||||||
"@types/fs-extra": "^9.0.12",
|
"@types/fs-extra": "^9.0.12",
|
||||||
"@types/js-yaml": "^4.0.2",
|
"@types/js-yaml": "^4.0.2",
|
||||||
"@types/node": "16.0.1",
|
"@types/node": "16.0.1",
|
||||||
"@types/sortablejs": "^1.10.7",
|
"@types/sortablejs": "^1.10.7",
|
||||||
"@types/webpack-env": "^1.16.2",
|
"@types/webpack-env": "^1.16.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||||
"@typescript-eslint/parser": "^4.28.5",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
"apply-loader": "2.0.0",
|
"apply-loader": "2.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"clone-deep": "^4.0.1",
|
"clone-deep": "^4.0.1",
|
||||||
"compare-versions": "^3.6.0",
|
"compare-versions": "^3.6.0",
|
||||||
"core-js": "^3.15.2",
|
"core-js": "^3.18.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "^6.2.0",
|
"css-loader": "^6.2.0",
|
||||||
"electron": "13.2.2",
|
"deep-equal": "2.0.5",
|
||||||
"electron-builder": "22.10.5",
|
"electron-builder": "^22.11.7",
|
||||||
|
"electron": "13.5.1",
|
||||||
"electron-download": "^4.1.1",
|
"electron-download": "^4.1.1",
|
||||||
"electron-installer-snap": "^5.1.0",
|
"electron-installer-snap": "^5.1.0",
|
||||||
"electron-notarize": "^1.0.1",
|
"electron-notarize": "^1.1.1",
|
||||||
"electron-rebuild": "^3.2.3",
|
"electron-rebuild": "^3.2.3",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"macos-release": "^3.0.1",
|
"macos-release": "^3.0.1",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-toastr": "^14.0.0",
|
"ngx-toastr": "^14.0.0",
|
||||||
"node-abi": "^3.0.0",
|
"node-abi": "^3.2.0",
|
||||||
"node-sass": "^6.0.1",
|
"node-sass": "^6.0.1",
|
||||||
"npmlog": "5.0.0",
|
"npmlog": "5.0.0",
|
||||||
"npx": "^10.2.2",
|
"npx": "^10.2.2",
|
||||||
@@ -59,20 +59,21 @@
|
|||||||
"sass-loader": "^12.1.0",
|
"sass-loader": "^12.1.0",
|
||||||
"shell-quote": "^1.7.2",
|
"shell-quote": "^1.7.2",
|
||||||
"shelljs": "0.8.4",
|
"shelljs": "0.8.4",
|
||||||
"slugify": "^1.6.0",
|
"slugify": "^1.6.1",
|
||||||
"sortablejs": "^1.14.0",
|
"sortablejs": "^1.14.0",
|
||||||
"source-code-pro": "^2.38.0",
|
"source-code-pro": "^2.38.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-sans-pro": "3.6.0",
|
"source-sans-pro": "3.6.0",
|
||||||
"ssh2": "^1.4.0",
|
"ssh2": "^1.5.0",
|
||||||
"style-loader": "^3.2.1",
|
"style-loader": "^3.2.1",
|
||||||
"svg-inline-loader": "^0.8.2",
|
"svg-inline-loader": "^0.8.2",
|
||||||
"ts-loader": "^9.2.3",
|
"ts-loader": "^9.2.3",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typedoc": "^0.21.6",
|
"typedoc": "^0.22.3",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
|
"utils-decorators": "^1.8.3",
|
||||||
"val-loader": "4.0.0",
|
"val-loader": "4.0.0",
|
||||||
"webpack": "^5.51.1",
|
"webpack": "^5.57.1",
|
||||||
"webpack-bundle-analyzer": "^4.4.2",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^4.8.0",
|
"webpack-cli": "^4.8.0",
|
||||||
"yaml-loader": "0.6.0",
|
"yaml-loader": "0.6.0",
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/node_modules/app-builder-lib/out/appInfo.js b/node_modules/app-builder-lib/out/appInfo.js
|
|
||||||
index 25a159e..bfe0590 100644
|
|
||||||
--- a/node_modules/app-builder-lib/out/appInfo.js
|
|
||||||
+++ b/node_modules/app-builder-lib/out/appInfo.js
|
|
||||||
@@ -165,7 +165,7 @@ class AppInfo {
|
|
||||||
get linuxPackageName() {
|
|
||||||
const name = this.name; // https://github.com/electron-userland/electron-builder/issues/2963
|
|
||||||
|
|
||||||
- return name.startsWith("@") ? this.sanitizedProductName : name;
|
|
||||||
+ return 'tabby-terminal';
|
|
||||||
}
|
|
||||||
|
|
||||||
get sanitizedName() {
|
|
15
patches/app-builder-lib+22.11.7.patch
Normal file
15
patches/app-builder-lib+22.11.7.patch
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
diff --git a/node_modules/app-builder-lib/out/appInfo.js b/node_modules/app-builder-lib/out/appInfo.js
|
||||||
|
index f241acc..2bddb7f 100644
|
||||||
|
--- a/node_modules/app-builder-lib/out/appInfo.js
|
||||||
|
+++ b/node_modules/app-builder-lib/out/appInfo.js
|
||||||
|
@@ -100,9 +100,7 @@ class AppInfo {
|
||||||
|
return this.info.metadata.name;
|
||||||
|
}
|
||||||
|
get linuxPackageName() {
|
||||||
|
- const name = this.name;
|
||||||
|
- // https://github.com/electron-userland/electron-builder/issues/2963
|
||||||
|
- return name.startsWith("@") ? this.sanitizedProductName : name;
|
||||||
|
+ return 'tabby-terminal'
|
||||||
|
}
|
||||||
|
get sanitizedName() {
|
||||||
|
return sanitizeFileName_1.sanitizeFileName(this.name);
|
@@ -18,6 +18,7 @@ sh.cd('..')
|
|||||||
|
|
||||||
sh.cd('web')
|
sh.cd('web')
|
||||||
sh.exec(`${npx} yarn install --force`)
|
sh.exec(`${npx} yarn install --force`)
|
||||||
|
sh.exec(`${npx} patch-package`)
|
||||||
sh.cd('..')
|
sh.cd('..')
|
||||||
|
|
||||||
vars.allPackages.forEach(plugin => {
|
vars.allPackages.forEach(plugin => {
|
||||||
|
@@ -27,6 +27,10 @@ export interface ToolbarButton {
|
|||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
submenuItems?: ToolbarButton[]
|
submenuItems?: ToolbarButton[]
|
||||||
|
|
||||||
|
showInToolbar?: boolean
|
||||||
|
|
||||||
|
showInStartPage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,8 +3,6 @@ import { Injectable } from '@angular/core'
|
|||||||
|
|
||||||
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
||||||
import { HostAppService, Platform } from './api/hostApp'
|
import { HostAppService, Platform } from './api/hostApp'
|
||||||
import { PartialProfile, Profile } from './api/profileProvider'
|
|
||||||
import { ConfigService } from './services/config.service'
|
|
||||||
import { HotkeysService } from './services/hotkeys.service'
|
import { HotkeysService } from './services/hotkeys.service'
|
||||||
import { ProfilesService } from './services/profiles.service'
|
import { ProfilesService } from './services/profiles.service'
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
constructor (
|
constructor (
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private profilesService: ProfilesService,
|
private profilesService: ProfilesService,
|
||||||
private config: ConfigService,
|
|
||||||
hotkeys: HotkeysService,
|
hotkeys: HotkeysService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@@ -28,31 +25,29 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
async activate () {
|
async activate () {
|
||||||
const profile = await this.profilesService.showProfileSelector()
|
const profile = await this.profilesService.showProfileSelector()
|
||||||
if (profile) {
|
if (profile) {
|
||||||
this.launchProfile(profile)
|
this.profilesService.launchProfile(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchProfile (profile: PartialProfile<Profile>) {
|
|
||||||
await this.profilesService.openNewTabForProfile(profile)
|
|
||||||
|
|
||||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
|
||||||
if (this.config.store.terminal.showRecentProfiles > 0) {
|
|
||||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
|
||||||
recentProfiles.unshift(profile)
|
|
||||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
|
||||||
} else {
|
|
||||||
recentProfiles = []
|
|
||||||
}
|
|
||||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
provide (): ToolbarButton[] {
|
provide (): ToolbarButton[] {
|
||||||
return [{
|
return [
|
||||||
icon: this.hostApp.platform === Platform.Web
|
{
|
||||||
? require('./icons/plus.svg')
|
icon: this.hostApp.platform === Platform.Web
|
||||||
: require('./icons/profiles.svg'),
|
? require('./icons/plus.svg')
|
||||||
title: 'New tab with profile',
|
: require('./icons/profiles.svg'),
|
||||||
click: () => this.activate(),
|
title: 'Profiles and connections',
|
||||||
}]
|
click: () => this.activate(),
|
||||||
|
},
|
||||||
|
...this.profilesService.getRecentProfiles().map(profile => ({
|
||||||
|
icon: require('./icons/history.svg'),
|
||||||
|
title: profile.name,
|
||||||
|
showInToolbar: false,
|
||||||
|
showinStartPage: true,
|
||||||
|
click: async () => {
|
||||||
|
const p = (await this.profilesService.getProfiles()).find(x => x.id === profile.id) ?? profile
|
||||||
|
this.profilesService.launchProfile(p)
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -111,6 +111,9 @@ export class AppRootComponent {
|
|||||||
if (hotkey === 'reopen-tab') {
|
if (hotkey === 'reopen-tab') {
|
||||||
this.app.reopenLastTab()
|
this.app.reopenLastTab()
|
||||||
}
|
}
|
||||||
|
if (hotkey === 'duplicate-tab') {
|
||||||
|
this.app.duplicateTab(this.app.activeTab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hotkey === 'toggle-fullscreen') {
|
if (hotkey === 'toggle-fullscreen') {
|
||||||
hostWindow.toggleFullscreen()
|
hostWindow.toggleFullscreen()
|
||||||
@@ -203,6 +206,7 @@ export class AppRootComponent {
|
|||||||
buttons = buttons.concat(provider.provide())
|
buttons = buttons.concat(provider.provide())
|
||||||
})
|
})
|
||||||
return buttons
|
return buttons
|
||||||
|
.filter(x => x.showInToolbar ?? true)
|
||||||
.filter(button => (button.weight ?? 0) > 0 === aboveZero)
|
.filter(button => (button.weight ?? 0) > 0 === aboveZero)
|
||||||
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
|
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, Subject, distinctUntilChanged, debounceTime } from 'rxjs'
|
import { Observable, Subject, distinctUntilChanged } from 'rxjs'
|
||||||
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
||||||
import { RecoveryToken } from '../api/tabRecovery'
|
import { RecoveryToken } from '../api/tabRecovery'
|
||||||
import { BaseComponent } from './base.component'
|
import { BaseComponent } from './base.component'
|
||||||
@@ -71,7 +71,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
get blurred$ (): Observable<void> { return this.blurred }
|
get blurred$ (): Observable<void> { return this.blurred }
|
||||||
get titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) }
|
get titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) }
|
||||||
get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) }
|
get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) }
|
||||||
get activity$ (): Observable<boolean> { return this.activity.pipe(debounceTime(500)) }
|
get activity$ (): Observable<boolean> { return this.activity }
|
||||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||||
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||||
|
|
||||||
@@ -113,16 +113,20 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
* Shows the acticity marker on the tab header
|
* Shows the acticity marker on the tab header
|
||||||
*/
|
*/
|
||||||
displayActivity (): void {
|
displayActivity (): void {
|
||||||
this.hasActivity = true
|
if (!this.hasActivity) {
|
||||||
this.activity.next(true)
|
this.hasActivity = true
|
||||||
|
this.activity.next(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the acticity marker from the tab header
|
* Removes the acticity marker from the tab header
|
||||||
*/
|
*/
|
||||||
clearActivity (): void {
|
clearActivity (): void {
|
||||||
this.hasActivity = false
|
if (this.hasActivity) {
|
||||||
this.activity.next(false)
|
this.hasActivity = false
|
||||||
|
this.activity.next(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -29,9 +29,11 @@ export class SelectorModalComponent<T> {
|
|||||||
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void {
|
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void {
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
this.selectedIndex--
|
this.selectedIndex--
|
||||||
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
this.selectedIndex++
|
this.selectedIndex++
|
||||||
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this.selectOption(this.filteredOptions[this.selectedIndex])
|
this.selectOption(this.filteredOptions[this.selectedIndex])
|
||||||
|
@@ -483,6 +483,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
newTab.parent = this
|
newTab.parent = this
|
||||||
this.recoveryStateChangedHint.next()
|
this.recoveryStateChangedHint.next()
|
||||||
this.onAfterTabAdded(newTab)
|
this.onAfterTabAdded(newTab)
|
||||||
|
this.updateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -612,6 +613,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearActivity (): void {
|
||||||
|
for (const tab of this.getAllTabs()) {
|
||||||
|
tab.clearActivity()
|
||||||
|
}
|
||||||
|
super.clearActivity()
|
||||||
|
}
|
||||||
|
|
||||||
private updateTitle (): void {
|
private updateTitle (): void {
|
||||||
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
|
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ div
|
|||||||
h1.tabby-title Tabby
|
h1.tabby-title Tabby
|
||||||
sup α
|
sup α
|
||||||
|
|
||||||
.list-group.list-group-light
|
.list-group.list-group-light.mb-4
|
||||||
a.list-group-item.list-group-item-action.d-flex(
|
a.list-group-item.list-group-item-action.d-flex(
|
||||||
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
|
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
|
||||||
(click)='button.click()',
|
(click)='button.click()',
|
||||||
|
@@ -25,6 +25,7 @@ export class StartPageComponent {
|
|||||||
return this.config.enabledServices(this.toolbarButtonProviders)
|
return this.config.enabledServices(this.toolbarButtonProviders)
|
||||||
.map(provider => provider.provide())
|
.map(provider => provider.provide())
|
||||||
.reduce((a, b) => a.concat(b))
|
.reduce((a, b) => a.concat(b))
|
||||||
|
.filter(x => x.showInStartPage ?? true)
|
||||||
.filter(x => !!x.click)
|
.filter(x => !!x.click)
|
||||||
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
|
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
h3.m-0 Vault is locked
|
h3.m-0 Vault is locked
|
||||||
.ml-auto(ngbDropdown, placement='bottom-right')
|
.ml-auto(ngbDropdown, placement='bottom-right')
|
||||||
button.btn.btn-link(ngbDropdownToggle, (click)='$event.stopPropagation()')
|
button.btn.btn-link(ngbDropdownToggle, (click)='$event.stopPropagation()')
|
||||||
span(*ngIf='rememberFor') Remember for {{rememberFor}} min
|
span(*ngIf='rememberFor') Remember for {{getRememberForDisplay(rememberFor)}}
|
||||||
span(*ngIf='!rememberFor') Do not remember
|
span(*ngIf='!rememberFor') Do not remember
|
||||||
div(ngbDropdownMenu)
|
div(ngbDropdownMenu)
|
||||||
button.dropdown-item(
|
button.dropdown-item(
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
button.dropdown-item(
|
button.dropdown-item(
|
||||||
*ngFor='let x of rememberOptions',
|
*ngFor='let x of rememberOptions',
|
||||||
(click)='rememberFor = x',
|
(click)='rememberFor = x',
|
||||||
) {{x}} min
|
) {{getRememberForDisplay(x)}}
|
||||||
|
|
||||||
.input-group
|
.input-group
|
||||||
input.form-control.form-control-lg(
|
input.form-control.form-control-lg(
|
||||||
|
@@ -8,7 +8,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
export class UnlockVaultModalComponent {
|
export class UnlockVaultModalComponent {
|
||||||
passphrase: string
|
passphrase: string
|
||||||
rememberFor = 1
|
rememberFor = 1
|
||||||
rememberOptions = [1, 5, 15, 60]
|
rememberOptions = [1, 5, 15, 60, 1440, 10080]
|
||||||
@ViewChild('input') input: ElementRef
|
@ViewChild('input') input: ElementRef
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@@ -33,4 +33,14 @@ export class UnlockVaultModalComponent {
|
|||||||
cancel (): void {
|
cancel (): void {
|
||||||
this.modalInstance.close(null)
|
this.modalInstance.close(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRememberForDisplay (rememberOption: number): string {
|
||||||
|
if (rememberOption >= 1440) {
|
||||||
|
return `${Math.round(rememberOption/1440*10)/10} day`
|
||||||
|
} else if (rememberOption >= 60) {
|
||||||
|
return `${Math.round(rememberOption/60*10)/10} hour`
|
||||||
|
} else {
|
||||||
|
return `${rememberOption} min`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ hotkeys:
|
|||||||
- 'Ctrl-Shift-PageDown'
|
- 'Ctrl-Shift-PageDown'
|
||||||
rearrange-panes:
|
rearrange-panes:
|
||||||
- 'Ctrl-Shift'
|
- 'Ctrl-Shift'
|
||||||
|
duplicate-tab: []
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
|
@@ -38,6 +38,7 @@ hotkeys:
|
|||||||
- '⌘-9'
|
- '⌘-9'
|
||||||
tab-10:
|
tab-10:
|
||||||
- '⌘-0'
|
- '⌘-0'
|
||||||
|
duplicate-tab: []
|
||||||
tab-11: []
|
tab-11: []
|
||||||
tab-12: []
|
tab-12: []
|
||||||
tab-13: []
|
tab-13: []
|
||||||
|
@@ -21,6 +21,7 @@ hotkeys:
|
|||||||
- 'Ctrl-Shift-PageDown'
|
- 'Ctrl-Shift-PageDown'
|
||||||
rearrange-panes:
|
rearrange-panes:
|
||||||
- 'Ctrl-Shift'
|
- 'Ctrl-Shift'
|
||||||
|
duplicate-tab: []
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
|
@@ -51,6 +51,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
id: 'rearrange-panes',
|
id: 'rearrange-panes',
|
||||||
name: 'Show pane labels (for rearranging)',
|
name: 'Show pane labels (for rearranging)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate-tab',
|
||||||
|
name: 'Duplicate tab',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tab-1',
|
id: 'tab-1',
|
||||||
name: 'Tab 1',
|
name: 'Tab 1',
|
||||||
|
1
tabby-core/src/icons/history.svg
Normal file
1
tabby-core/src/icons/history.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="history" class="svg-inline--fa fa-history fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#fff" d="M504 255.532c.252 136.64-111.182 248.372-247.822 248.468-64.014.045-122.373-24.163-166.394-63.942-5.097-4.606-5.3-12.543-.443-17.4l16.96-16.96c4.529-4.529 11.776-4.659 16.555-.395C158.208 436.843 204.848 456 256 456c110.549 0 200-89.468 200-200 0-110.549-89.468-200-200-200-55.52 0-105.708 22.574-141.923 59.043l49.091 48.413c7.641 7.535 2.305 20.544-8.426 20.544H26.412c-6.627 0-12-5.373-12-12V45.443c0-10.651 12.843-16.023 20.426-8.544l45.097 44.474C124.866 36.067 187.15 8 256 8c136.811 0 247.747 110.781 248 247.532zm-167.058 90.173l14.116-19.409c3.898-5.36 2.713-12.865-2.647-16.763L280 259.778V116c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v168.222l88.179 64.13c5.36 3.897 12.865 2.712 16.763-2.647z"></path></svg>
|
After Width: | Height: | Size: 940 B |
@@ -145,6 +145,8 @@ export class HotkeysService {
|
|||||||
})
|
})
|
||||||
this.recognitionPhase = false
|
this.recognitionPhase = false
|
||||||
}
|
}
|
||||||
|
this.pressedKeys.clear()
|
||||||
|
this.pressedKeyTimestamps.clear()
|
||||||
this.removePressedKey(keyName)
|
this.removePressedKey(keyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +231,9 @@ export class HotkeysService {
|
|||||||
if (!matches.length) {
|
if (!matches.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (matches[0].sequence.length > 1) {
|
||||||
|
this.clearCurrentKeystrokes()
|
||||||
|
}
|
||||||
return matches[0].id
|
return matches[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +298,7 @@ export class HotkeysService {
|
|||||||
this._hotkey.next(hotkey)
|
this._hotkey.next(hotkey)
|
||||||
this.pressedHotkey = hotkey
|
this.pressedHotkey = hotkey
|
||||||
}
|
}
|
||||||
|
this.recognitionPhase = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitHotkeyOff (hotkey: string) {
|
private emitHotkeyOff (hotkey: string) {
|
||||||
|
@@ -37,13 +37,6 @@ export class ProfilesService {
|
|||||||
if (params) {
|
if (params) {
|
||||||
const tab = this.app.openNewTab(params)
|
const tab = this.app.openNewTab(params)
|
||||||
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
;(this.app.getParentTab(tab) ?? tab).color = profile.color ?? null
|
||||||
|
|
||||||
if (profile.name) {
|
|
||||||
tab.setTitle(profile.name)
|
|
||||||
}
|
|
||||||
if (profile.disableDynamicTitle) {
|
|
||||||
tab['disableDynamicTitle'] = true
|
|
||||||
}
|
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -51,7 +44,15 @@ export class ProfilesService {
|
|||||||
|
|
||||||
async newTabParametersForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>|null> {
|
async newTabParametersForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>|null> {
|
||||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||||
return this.providerForProfile(fullProfile)?.getNewTabParameters(fullProfile) ?? null
|
const params = await this.providerForProfile(fullProfile)?.getNewTabParameters(fullProfile) ?? null
|
||||||
|
if (params) {
|
||||||
|
params.inputs ??= {}
|
||||||
|
params.inputs['title'] = profile.name
|
||||||
|
if (profile.disableDynamicTitle) {
|
||||||
|
params.inputs['disableDynamicTitle'] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviders (): ProfileProvider<Profile>[] {
|
getProviders (): ProfileProvider<Profile>[] {
|
||||||
@@ -89,11 +90,16 @@ export class ProfilesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||||
|
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||||
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
return recentProfiles
|
||||||
|
}
|
||||||
|
|
||||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||||
return new Promise<PartialProfile<Profile>|null>(async (resolve, reject) => {
|
return new Promise<PartialProfile<Profile>|null>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
const recentProfiles = this.getRecentProfiles()
|
||||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
|
||||||
|
|
||||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||||
...this.selectorOptionForProfile(p),
|
...this.selectorOptionForProfile(p),
|
||||||
@@ -188,4 +194,18 @@ export class ProfilesService {
|
|||||||
].reduce(configMerge, {})
|
].reduce(configMerge, {})
|
||||||
return new ConfigProxy(profile, defaults) as unknown as T
|
return new ConfigProxy(profile, defaults) as unknown as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||||
|
await this.openNewTabForProfile(profile)
|
||||||
|
|
||||||
|
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||||
|
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||||
|
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||||
|
recentProfiles.unshift(profile)
|
||||||
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
} else {
|
||||||
|
recentProfiles = []
|
||||||
|
}
|
||||||
|
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,10 +59,10 @@ export default class ElectronModule {
|
|||||||
|
|
||||||
themeService.themeChanged$.subscribe(theme => {
|
themeService.themeChanged$.subscribe(theme => {
|
||||||
if (hostApp.platform === Platform.macOS) {
|
if (hostApp.platform === Platform.macOS) {
|
||||||
hostWindow.getWindow().setTrafficLightPosition({
|
hostWindow.setTrafficLightPosition(
|
||||||
x: theme.macOSWindowButtonsInsetX ?? 14,
|
theme.macOSWindowButtonsInsetX ?? 14,
|
||||||
y: theme.macOSWindowButtonsInsetY ?? 11,
|
theme.macOSWindowButtonsInsetY ?? 11,
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ export default class ElectronModule {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
hostWindow.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
|
hostWindow.setProgressBar(progress / 100.0)
|
||||||
} else {
|
} else {
|
||||||
hostWindow.getWindow().setProgressBar(-1, { mode: 'none' })
|
hostWindow.setProgressBar(-1)
|
||||||
}
|
}
|
||||||
lastProgress = progress
|
lastProgress = progress
|
||||||
})
|
})
|
||||||
@@ -116,7 +116,7 @@ export default class ElectronModule {
|
|||||||
document.body.classList.toggle('vibrant', this.config.store.appearance.vibrancy)
|
document.body.classList.toggle('vibrant', this.config.store.appearance.vibrancy)
|
||||||
this.electron.ipcRenderer.send('window-set-vibrancy', this.config.store.appearance.vibrancy, vibrancyType)
|
this.electron.ipcRenderer.send('window-set-vibrancy', this.config.store.appearance.vibrancy, vibrancyType)
|
||||||
|
|
||||||
this.hostWindow.getWindow().setOpacity(this.config.store.appearance.opacity)
|
this.hostWindow.setOpacity(this.config.store.appearance.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -97,6 +97,18 @@ export class ElectronHostWindow extends HostWindowService {
|
|||||||
this.getWindow().setTouchBar(touchBar)
|
this.getWindow().setTouchBar(touchBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTrafficLightPosition (x: number, y: number): void {
|
||||||
|
this.electron.ipcRenderer.send('window-set-traffic-light-position', x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpacity (opacity: number): void {
|
||||||
|
this.electron.ipcRenderer.send('window-set-opacity', opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgressBar (value: number): void {
|
||||||
|
this.electron.ipcRenderer.send('window-set-progress-bar', value)
|
||||||
|
}
|
||||||
|
|
||||||
bringToFront (): void {
|
bringToFront (): void {
|
||||||
this.electron.ipcRenderer.send('window-bring-to-front')
|
this.electron.ipcRenderer.send('window-bring-to-front')
|
||||||
}
|
}
|
||||||
|
@@ -24,8 +24,7 @@
|
|||||||
"dataurl": "0.1.0",
|
"dataurl": "0.1.0",
|
||||||
"hasbin": "^1.2.3",
|
"hasbin": "^1.2.3",
|
||||||
"ps-node": "^0.1.6",
|
"ps-node": "^0.1.6",
|
||||||
"runes": "^0.4.2",
|
"runes": "^0.4.2"
|
||||||
"utils-decorators": "^1.8.3"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/animations": "^9.1.9",
|
"@angular/animations": "^9.1.9",
|
||||||
|
@@ -3,7 +3,11 @@ ng-container(*ngIf='!argvMode')
|
|||||||
label Command line
|
label Command line
|
||||||
.input-group
|
.input-group
|
||||||
.input-group-prepend
|
.input-group-prepend
|
||||||
button.btn.btn-secondary((click)='switchToArgv()', title='Switch to split arguments')
|
a.input-group-text(
|
||||||
|
(click)='switchToArgv()',
|
||||||
|
ngbTooltip='Split into unescaped arguments',
|
||||||
|
href='#'
|
||||||
|
)
|
||||||
i.fas.fa-fw.fa-caret-right
|
i.fas.fa-fw.fa-caret-right
|
||||||
input.form-control.text-monospace(
|
input.form-control.text-monospace(
|
||||||
[(ngModel)]='command',
|
[(ngModel)]='command',
|
||||||
@@ -15,7 +19,11 @@ ng-container(*ngIf='argvMode')
|
|||||||
label Program
|
label Program
|
||||||
.input-group
|
.input-group
|
||||||
.input-group-prepend
|
.input-group-prepend
|
||||||
button.btn.btn-secondary((click)='switchToCommand()', title='Switch to a single-line command')
|
a.input-group-text(
|
||||||
|
(click)='switchToCommand()',
|
||||||
|
ngbTooltip='Combine into a single escaped command',
|
||||||
|
href='#'
|
||||||
|
)
|
||||||
i.fas.fa-fw.fa-caret-down
|
i.fas.fa-fw.fa-caret-down
|
||||||
input.form-control.text-monospace(
|
input.form-control.text-monospace(
|
||||||
type='text',
|
type='text',
|
||||||
|
@@ -8,6 +8,13 @@
|
|||||||
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
|
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
|
||||||
i.fas.fa-fw.fa-trash
|
i.fas.fa-fw.fa-trash
|
||||||
|
|
||||||
button.btn.btn-secondary((click)='addEnvironmentVar()')
|
.d-flex
|
||||||
i.fas.fa-plus.mr-2
|
button.btn.btn-secondary((click)='addEnvironmentVar()')
|
||||||
span Add
|
i.fas.fa-plus.mr-2
|
||||||
|
span Add
|
||||||
|
|
||||||
|
.ml-auto
|
||||||
|
.text-muted Substitutions allowed.
|
||||||
|
.d-flex.ml-1(*ngIf='shouldShowExample()')
|
||||||
|
.text-muted Example:
|
||||||
|
a.ml-1((click)='addExample()', href='#') extend PATH
|
||||||
|
@@ -44,4 +44,13 @@ export class EnvironmentEditorComponent {
|
|||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldShowExample (): boolean {
|
||||||
|
return !this.vars.find(v => v.key.toLowerCase() === 'path')
|
||||||
|
}
|
||||||
|
|
||||||
|
addExample (): void {
|
||||||
|
const value = process.platform === 'win32' ? 'C:\\Program Files\\Custom:%PATH%' : '/opt/custom:$PATH'
|
||||||
|
this.vars.push({ key: 'PATH', value })
|
||||||
|
this.emitUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
import { WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild, ConfigService } from 'tabby-core'
|
import { WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild, ConfigService } from 'tabby-core'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -9,6 +9,8 @@ export class ShellSettingsTabComponent {
|
|||||||
isConPTYAvailable: boolean
|
isConPTYAvailable: boolean
|
||||||
isConPTYStable: boolean
|
isConPTYStable: boolean
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
) {
|
) {
|
||||||
|
@@ -58,7 +58,10 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
|||||||
|
|
||||||
initializeSession (columns: number, rows: number): void {
|
initializeSession (columns: number, rows: number): void {
|
||||||
if (this.profile.options.runAsAdministrator && this.uac.isAvailable) {
|
if (this.profile.options.runAsAdministrator && this.uac.isAvailable) {
|
||||||
this.profile.options = this.uac.patchSessionOptionsForUAC(this.profile.options)
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
options: this.uac.patchSessionOptionsForUAC(this.profile.options),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session!.start({
|
this.session!.start({
|
||||||
|
@@ -81,6 +81,35 @@ export class PTYProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeEnv (...envs) {
|
||||||
|
const result = {}
|
||||||
|
const keyMap = {}
|
||||||
|
for (const env of envs) {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
// const lookup = process.platform === 'win32' ? key.toLowerCase() : key
|
||||||
|
const lookup = key.toLowerCase()
|
||||||
|
keyMap[lookup] ??= key
|
||||||
|
result[keyMap[lookup]] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteEnv (env: Record<string, string>) {
|
||||||
|
env = { ...env }
|
||||||
|
const pattern = process.platform === 'win32' ? /%(\w+)%/g : /\$(\w+)\b/g
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
env[key] = value.replace(pattern, function (substring, p1) {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return Object.entries(process.env).find(x => x[0].toLowerCase() === p1.toLowerCase())?.[1] ?? ''
|
||||||
|
} else {
|
||||||
|
return process.env[p1] ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
export class Session extends BaseSession {
|
export class Session extends BaseSession {
|
||||||
private pty: PTYProxy|null = null
|
private pty: PTYProxy|null = null
|
||||||
@@ -108,22 +137,18 @@ export class Session extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!pty) {
|
if (!pty) {
|
||||||
const env = {
|
let env = mergeEnv(
|
||||||
...process.env,
|
process.env,
|
||||||
TERM: 'xterm-256color',
|
{
|
||||||
TERM_PROGRAM: 'Tabby',
|
TERM: 'xterm-256color',
|
||||||
...options.env,
|
TERM_PROGRAM: 'Tabby',
|
||||||
...this.config.store.terminal.environment || {},
|
},
|
||||||
}
|
substituteEnv(options.env ?? {}),
|
||||||
|
this.config.store.terminal.environment || {},
|
||||||
|
)
|
||||||
|
|
||||||
if (this.hostApp.platform === Platform.Windows && this.config.store.terminal.setComSpec) {
|
if (this.hostApp.platform === Platform.Windows && this.config.store.terminal.setComSpec) {
|
||||||
for (const k of Object.keys(env)) {
|
env = mergeEnv(env, { COMSPEC: this.bootstrapData.executable })
|
||||||
if (k.toUpperCase() === 'COMSPEC') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete env[k]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
env.COMSPEC = this.bootstrapData.executable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete env['']
|
delete env['']
|
||||||
|
@@ -65,15 +65,3 @@ tiny-inflate@^1.0.3:
|
|||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
||||||
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
||||||
|
|
||||||
tinyqueue@^2.0.3:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
|
|
||||||
integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
|
|
||||||
|
|
||||||
utils-decorators@^1.8.3:
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/utils-decorators/-/utils-decorators-1.10.0.tgz#eb9208ccbb7fbb7488d5d04b2611df62c2fcaf4d"
|
|
||||||
integrity sha512-wlNRoPCFdxSReLfmhNqkZsg8FqsKu9d5trdSELxBZCtmK4KPtSidxRg24+bpZQjEBBF0hUIQEFz2uM7sBDVG2Q==
|
|
||||||
dependencies:
|
|
||||||
tinyqueue "^2.0.3"
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rxjs'
|
import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rxjs'
|
||||||
import semverGt from 'semver/functions/gt'
|
import semverGt from 'semver/functions/gt'
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, HostBinding, Input } from '@angular/core'
|
||||||
import { ConfigService, PlatformService, PluginInfo } from 'tabby-core'
|
import { ConfigService, PlatformService, PluginInfo } from 'tabby-core'
|
||||||
import { PluginManagerService } from '../services/pluginManager.service'
|
import { PluginManagerService } from '../services/pluginManager.service'
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ export class PluginsSettingsTabComponent {
|
|||||||
@Input() erroredPlugin: string
|
@Input() erroredPlugin: string
|
||||||
@Input() errorMessage: string
|
@Input() errorMessage: string
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
|
@@ -8,6 +8,7 @@ const OFFICIAL_NPM_ACCOUNT = 'eugenepankov'
|
|||||||
|
|
||||||
const BLACKLIST = [
|
const BLACKLIST = [
|
||||||
'terminus-shell-selector', // superseded by profiles
|
'terminus-shell-selector', // superseded by profiles
|
||||||
|
'terminus-scrollbar', // now useless
|
||||||
]
|
]
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
@@ -18,8 +18,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"marked": "^3.0.2",
|
"marked": "^3.0.2",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1"
|
||||||
"utils-decorators": "^1.8.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/animations": "^9.1.9",
|
"@angular/animations": "^9.1.9",
|
||||||
|
@@ -5,6 +5,8 @@ export abstract class SettingsTabProvider {
|
|||||||
id: string
|
id: string
|
||||||
icon: string
|
icon: string
|
||||||
title: string
|
title: string
|
||||||
|
weight = 0
|
||||||
|
prioritized = false
|
||||||
|
|
||||||
getComponentType (): any {
|
getComponentType (): any {
|
||||||
return null
|
return null
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { BaseComponent, ConfigService, PromptModalComponent, HostAppService, PlatformService, NotificationsService } from 'tabby-core'
|
import { BaseComponent, ConfigService, PromptModalComponent, HostAppService, PlatformService, NotificationsService } from 'tabby-core'
|
||||||
import { Config, ConfigSyncService } from '../services/configSync.service'
|
import { Config, ConfigSyncService } from '../services/configSync.service'
|
||||||
@@ -15,6 +15,8 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
|||||||
connectionError: Error|null = null
|
connectionError: Error|null = null
|
||||||
configs: Config[]|null = null
|
configs: Config[]|null = null
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public platform: PlatformService,
|
public platform: PlatformService,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
import deepClone from 'clone-deep'
|
import deepClone from 'clone-deep'
|
||||||
import { Component, Inject } from '@angular/core'
|
import { Component, HostBinding, Inject } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider } from 'tabby-core'
|
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider } from 'tabby-core'
|
||||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||||
@@ -25,6 +25,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||||||
profileGroups: ProfileGroup[]
|
profileGroups: ProfileGroup[]
|
||||||
filter = ''
|
filter = ''
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
|
|
||||||
|
|
||||||
.content
|
.content
|
||||||
ul.nav-pills(ngbNav, #nav='ngbNav', [activeId]='activeTab', orientation='vertical')
|
ul.nav-pills(ngbNav, #nav='ngbNav', [activeId]='activeTab', orientation='vertical')
|
||||||
li(ngbNavItem='application')
|
li(ngbNavItem='application')
|
||||||
@@ -7,80 +5,92 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
|
|||||||
i.fas.fa-fw.fa-window-maximize.mr-2
|
i.fas.fa-fw.fa-window-maximize.mr-2
|
||||||
| Application
|
| Application
|
||||||
ng-template(ngbNavContent)
|
ng-template(ngbNavContent)
|
||||||
.tabby-logo.mt-3
|
.content-box
|
||||||
h1.tabby-title Tabby
|
.tabby-logo.mt-3
|
||||||
sup α
|
h1.tabby-title Tabby
|
||||||
|
sup α
|
||||||
|
|
||||||
.text-center
|
.text-center
|
||||||
.text-muted {{homeBase.appVersion}}
|
.text-muted {{homeBase.appVersion}}
|
||||||
|
|
||||||
.mb-5.mt-3
|
.mb-5.mt-3
|
||||||
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.openGitHub()')
|
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.openGitHub()')
|
||||||
i.fab.fa-github
|
i.fab.fa-github
|
||||||
span GitHub
|
span GitHub
|
||||||
|
|
||||||
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.reportBug()')
|
button.btn.btn-secondary.mr-3.mb-2((click)='homeBase.reportBug()')
|
||||||
i.fas.fa-bug
|
i.fas.fa-bug
|
||||||
span Report a problem
|
span Report a problem
|
||||||
|
|
||||||
button.btn.btn-secondary.mr-3.mb-2(
|
button.btn.btn-secondary.mr-3.mb-2(
|
||||||
(click)='showReleaseNotes()',
|
(click)='showReleaseNotes()',
|
||||||
)
|
|
||||||
i.fas.fa-book
|
|
||||||
span What's new
|
|
||||||
|
|
||||||
button.btn.btn-secondary.mr-3.mb-2(
|
|
||||||
*ngIf='!updateAvailable && hostApp.platform !== Platform.Web',
|
|
||||||
(click)='checkForUpdates()',
|
|
||||||
[disabled]='checkingForUpdate'
|
|
||||||
)
|
|
||||||
i.fas.fa-sync(
|
|
||||||
[class.fa-spin]='checkingForUpdate'
|
|
||||||
)
|
)
|
||||||
span Check for updates
|
i.fas.fa-book
|
||||||
|
span What's new
|
||||||
|
|
||||||
button.btn.btn-info.mr-3.mb-2(
|
button.btn.btn-secondary.mr-3.mb-2(
|
||||||
*ngIf='updateAvailable',
|
*ngIf='!updateAvailable && hostApp.platform !== Platform.Web',
|
||||||
(click)='updater.update()',
|
(click)='checkForUpdates()',
|
||||||
|
[disabled]='checkingForUpdate'
|
||||||
|
)
|
||||||
|
i.fas.fa-sync(
|
||||||
|
[class.fa-spin]='checkingForUpdate'
|
||||||
|
)
|
||||||
|
span Check for updates
|
||||||
|
|
||||||
|
button.btn.btn-info.mr-3.mb-2(
|
||||||
|
*ngIf='updateAvailable',
|
||||||
|
(click)='updater.update()',
|
||||||
|
)
|
||||||
|
i.fas.fa-sync
|
||||||
|
span Update
|
||||||
|
|
||||||
|
.form-line(*ngIf='platform.isShellIntegrationSupported()')
|
||||||
|
.header
|
||||||
|
.title Shell integration
|
||||||
|
.description Allows quickly opening a terminal in the selected folder
|
||||||
|
toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()')
|
||||||
|
|
||||||
|
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
||||||
|
.header
|
||||||
|
.title Enable analytics
|
||||||
|
.description We're only tracking your Tabby and OS versions.
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.enableAnalytics',
|
||||||
|
(ngModelChange)='saveConfiguration(true)',
|
||||||
)
|
)
|
||||||
i.fas.fa-sync
|
|
||||||
span Update
|
|
||||||
|
|
||||||
.form-line(*ngIf='platform.isShellIntegrationSupported()')
|
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
||||||
.header
|
.header
|
||||||
.title Shell integration
|
.title Automatic Updates
|
||||||
.description Allows quickly opening a terminal in the selected folder
|
.description Enable automatic installation of updates when they become available.
|
||||||
toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()')
|
toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()')
|
||||||
|
|
||||||
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
||||||
.header
|
.header
|
||||||
.title Enable analytics
|
.title Debugging
|
||||||
.description We're only tracking your Tabby and OS versions.
|
|
||||||
toggle(
|
|
||||||
[(ngModel)]='config.store.enableAnalytics',
|
|
||||||
(ngModelChange)='saveConfiguration(true)',
|
|
||||||
)
|
|
||||||
|
|
||||||
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
button.btn.btn-secondary((click)='hostWindow.openDevTools()')
|
||||||
.header
|
i.fas.fa-bug
|
||||||
.title Automatic Updates
|
span Open DevTools
|
||||||
.description Enable automatic installation of updates when they become available.
|
|
||||||
toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()')
|
|
||||||
|
|
||||||
.form-line(*ngIf='hostApp.platform !== Platform.Web')
|
ng-container(*ngFor='let provider of settingsProviders')
|
||||||
.header
|
li(*ngIf='provider.prioritized', [ngbNavItem]='provider.id')
|
||||||
.title Debugging
|
a(ngbNavLink)
|
||||||
|
i(class='fas fa-fw mr-2 fa-{{provider.icon}}')
|
||||||
|
| {{provider.title}}
|
||||||
|
ng-template(ngbNavContent)
|
||||||
|
settings-tab-body([provider]='provider')
|
||||||
|
|
||||||
button.btn.btn-secondary((click)='hostWindow.openDevTools()')
|
.mb-3
|
||||||
i.fas.fa-bug
|
|
||||||
span Open DevTools
|
|
||||||
|
|
||||||
li(*ngFor='let provider of settingsProviders', [ngbNavItem]='provider.id')
|
ng-container(*ngFor='let provider of settingsProviders')
|
||||||
a(ngbNavLink)
|
li(*ngIf='!provider.prioritized', [ngbNavItem]='provider.id')
|
||||||
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
|
a(ngbNavLink)
|
||||||
| {{provider.title}}
|
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
|
||||||
ng-template(ngbNavContent)
|
| {{provider.title}}
|
||||||
settings-tab-body([provider]='provider')
|
ng-template(ngbNavContent)
|
||||||
|
settings-tab-body([provider]='provider')
|
||||||
|
|
||||||
li(ngbNavItem='config-file')
|
li(ngbNavItem='config-file')
|
||||||
a(ngbNavLink)
|
a(ngbNavLink)
|
||||||
@@ -118,3 +128,5 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
|
|||||||
| Show config file
|
| Show config file
|
||||||
|
|
||||||
div([ngbNavOutlet]='nav')
|
div([ngbNavOutlet]='nav')
|
||||||
|
|
||||||
|
button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='restartApp()') Restart the app to apply changes
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
> .nav {
|
> .nav {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
width: 190px;
|
width: 212px;
|
||||||
flex: none;
|
flex: none;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
|
|
||||||
> ::ng-deep .tab-pane {
|
> ::ng-deep .tab-pane {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
> settings-tab-body > * {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
|||||||
this.setTitle('Settings')
|
this.setTitle('Settings')
|
||||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||||
this.settingsProviders = this.settingsProviders.filter(x => !!x.getComponentType())
|
this.settingsProviders = this.settingsProviders.filter(x => !!x.getComponentType())
|
||||||
this.settingsProviders.sort((a, b) => a.title.localeCompare(b.title))
|
this.settingsProviders.sort((a, b) => a.weight - b.weight + a.title.localeCompare(b.title))
|
||||||
|
|
||||||
this.configDefaults = yaml.dump(config.getDefaults())
|
this.configDefaults = yaml.dump(config.getDefaults())
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { BaseComponent, VaultService, VaultSecret, Vault, PlatformService, ConfigService, VAULT_SECRET_TYPE_FILE, PromptModalComponent, VaultFileSecret } from 'tabby-core'
|
import { BaseComponent, VaultService, VaultSecret, Vault, PlatformService, ConfigService, VAULT_SECRET_TYPE_FILE, PromptModalComponent, VaultFileSecret } from 'tabby-core'
|
||||||
import { SetVaultPassphraseModalComponent } from './setVaultPassphraseModal.component'
|
import { SetVaultPassphraseModalComponent } from './setVaultPassphraseModal.component'
|
||||||
@@ -14,6 +14,8 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
|||||||
vaultContents: Vault|null = null
|
vaultContents: Vault|null = null
|
||||||
VAULT_SECRET_TYPE_FILE = VAULT_SECRET_TYPE_FILE
|
VAULT_SECRET_TYPE_FILE = VAULT_SECRET_TYPE_FILE
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public vault: VaultService,
|
public vault: VaultService,
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { debounce } from 'utils-decorators/dist/esm/debounce/debounce'
|
import { debounce } from 'utils-decorators/dist/esm/debounce/debounce'
|
||||||
import { Component, Inject, NgZone, Optional } from '@angular/core'
|
import { Component, HostBinding, Inject, NgZone, Optional } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
DockingService,
|
DockingService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
@@ -25,6 +25,8 @@ export class WindowSettingsTabComponent extends BaseComponent {
|
|||||||
Platform = Platform
|
Platform = Platform
|
||||||
isFluentVibrancySupported = false
|
isFluentVibrancySupported = false
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
|
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#fff" d="M482.696 299.276l-32.61-18.827a195.168 195.168 0 0 0 0-48.899l32.61-18.827c9.576-5.528 14.195-16.902 11.046-27.501-11.214-37.749-31.175-71.728-57.535-99.595-7.634-8.07-19.817-9.836-29.437-4.282l-32.562 18.798a194.125 194.125 0 0 0-42.339-24.48V38.049c0-11.13-7.652-20.804-18.484-23.367-37.644-8.909-77.118-8.91-114.77 0-10.831 2.563-18.484 12.236-18.484 23.367v37.614a194.101 194.101 0 0 0-42.339 24.48L105.23 81.345c-9.621-5.554-21.804-3.788-29.437 4.282-26.36 27.867-46.321 61.847-57.535 99.595-3.149 10.599 1.47 21.972 11.046 27.501l32.61 18.827a195.168 195.168 0 0 0 0 48.899l-32.61 18.827c-9.576 5.528-14.195 16.902-11.046 27.501 11.214 37.748 31.175 71.728 57.535 99.595 7.634 8.07 19.817 9.836 29.437 4.283l32.562-18.798a194.08 194.08 0 0 0 42.339 24.479v37.614c0 11.13 7.652 20.804 18.484 23.367 37.645 8.909 77.118 8.91 114.77 0 10.831-2.563 18.484-12.236 18.484-23.367v-37.614a194.138 194.138 0 0 0 42.339-24.479l32.562 18.798c9.62 5.554 21.803 3.788 29.437-4.283 26.36-27.867 46.321-61.847 57.535-99.595 3.149-10.599-1.47-21.972-11.046-27.501zm-65.479 100.461l-46.309-26.74c-26.988 23.071-36.559 28.876-71.039 41.059v53.479a217.145 217.145 0 0 1-87.738 0v-53.479c-33.621-11.879-43.355-17.395-71.039-41.059l-46.309 26.74c-19.71-22.09-34.689-47.989-43.929-75.958l46.329-26.74c-6.535-35.417-6.538-46.644 0-82.079l-46.329-26.74c9.24-27.969 24.22-53.869 43.929-75.969l46.309 26.76c27.377-23.434 37.063-29.065 71.039-41.069V44.464a216.79 216.79 0 0 1 87.738 0v53.479c33.978 12.005 43.665 17.637 71.039 41.069l46.309-26.76c19.709 22.099 34.689 47.999 43.929 75.969l-46.329 26.74c6.536 35.426 6.538 46.644 0 82.079l46.329 26.74c-9.24 27.968-24.219 53.868-43.929 75.957zM256 160c-52.935 0-96 43.065-96 96s43.065 96 96 96 96-43.065 96-96-43.065-96-96-96zm0 160c-35.29 0-64-28.71-64-64s28.71-64 64-64 64 28.71 64 64-28.71 64-64 64z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#fff" d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 909 B |
@@ -51,7 +51,8 @@ export class VaultSettingsTabProvider extends SettingsTabProvider {
|
|||||||
export class ProfilesSettingsTabProvider extends SettingsTabProvider {
|
export class ProfilesSettingsTabProvider extends SettingsTabProvider {
|
||||||
id = 'profiles'
|
id = 'profiles'
|
||||||
icon = 'window-restore'
|
icon = 'window-restore'
|
||||||
title = 'Profiles'
|
title = 'Profiles & connections'
|
||||||
|
prioritized = true
|
||||||
|
|
||||||
getComponentType (): any {
|
getComponentType (): any {
|
||||||
return ProfilesSettingsTabComponent
|
return ProfilesSettingsTabComponent
|
||||||
|
@@ -24,15 +24,3 @@ opencollective-postinstall@^2.0.2:
|
|||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
|
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
|
||||||
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
|
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
|
||||||
|
|
||||||
tinyqueue@^2.0.3:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
|
|
||||||
integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
|
|
||||||
|
|
||||||
utils-decorators@^1.8.0:
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/utils-decorators/-/utils-decorators-1.10.0.tgz#eb9208ccbb7fbb7488d5d04b2611df62c2fcaf4d"
|
|
||||||
integrity sha512-wlNRoPCFdxSReLfmhNqkZsg8FqsKu9d5trdSELxBZCtmK4KPtSidxRg24+bpZQjEBBF0hUIQEFz2uM7sBDVG2Q==
|
|
||||||
dependencies:
|
|
||||||
tinyqueue "^2.0.3"
|
|
||||||
|
@@ -25,6 +25,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
label Username
|
label Username
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
|
placeholder='Ask every time',
|
||||||
[(ngModel)]='profile.options.user',
|
[(ngModel)]='profile.options.user',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
i.far.fa-keyboard
|
i.far.fa-keyboard
|
||||||
.m-0 Interactive
|
.m-0 Interactive
|
||||||
|
|
||||||
.form-line(*ngIf='!profile.options.auth || profile.options.auth === "password"')
|
.form-line(*ngIf='profile.options.user && (!profile.options.auth || profile.options.auth === "password")')
|
||||||
.header
|
.header
|
||||||
.title Password
|
.title Password
|
||||||
.description(*ngIf='!hasSavedPassword') Save a password in the keychain
|
.description(*ngIf='!hasSavedPassword') Save a password in the keychain
|
||||||
|
@@ -44,10 +44,12 @@ export class SSHProfileSettingsComponent {
|
|||||||
this.profile.options.privateKeys ??= []
|
this.profile.options.privateKeys ??= []
|
||||||
|
|
||||||
this.useProxyCommand = !!this.profile.options.proxyCommand
|
this.useProxyCommand = !!this.profile.options.proxyCommand
|
||||||
try {
|
if (this.profile.options.user) {
|
||||||
this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.profile)
|
try {
|
||||||
} catch (e) {
|
this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.profile)
|
||||||
console.error('Could not check for saved password', e)
|
} catch (e) {
|
||||||
|
console.error('Could not check for saved password', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -42,4 +42,15 @@ h3 SSH
|
|||||||
(ngModelChange)='config.save()',
|
(ngModelChange)='config.save()',
|
||||||
)
|
)
|
||||||
|
|
||||||
.alert.alert-info SSH connection management is now done through the Profiles tab
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Override X11 display
|
||||||
|
.description Path or address of the local X11 socket
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[placeholder]='defaultX11Display',
|
||||||
|
[(ngModel)]='config.store.ssh.x11Display',
|
||||||
|
(ngModelChange)='config.save()'
|
||||||
|
)
|
||||||
|
|
||||||
|
.alert.alert-info SSH connection management is now done through the #[strong Profiles & connections] tab
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
|
import { X11Socket } from '../session/x11'
|
||||||
import { ConfigService, HostAppService, Platform } from 'tabby-core'
|
import { ConfigService, HostAppService, Platform } from 'tabby-core'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -7,9 +8,19 @@ import { ConfigService, HostAppService, Platform } from 'tabby-core'
|
|||||||
})
|
})
|
||||||
export class SSHSettingsTabComponent {
|
export class SSHSettingsTabComponent {
|
||||||
Platform = Platform
|
Platform = Platform
|
||||||
|
defaultX11Display: string
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
) { }
|
) {
|
||||||
|
const spec = X11Socket.resolveDisplaySpec()
|
||||||
|
if ('path' in spec) {
|
||||||
|
this.defaultX11Display = spec.path
|
||||||
|
} else {
|
||||||
|
this.defaultX11Display = `${spec.host}:${spec.port}`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -81,7 +81,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupOneSession (session: SSHSession): Promise<void> {
|
async setupOneSession (session: SSHSession, interactive: boolean): Promise<void> {
|
||||||
if (session.profile.options.jumpHost) {
|
if (session.profile.options.jumpHost) {
|
||||||
const jumpConnection: PartialProfile<SSHProfile>|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
|
const jumpConnection: PartialProfile<SSHProfile>|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
this.profilesService.getConfigProxyForProfile(jumpConnection)
|
this.profilesService.getConfigProxyForProfile(jumpConnection)
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.setupOneSession(jumpSession)
|
await this.setupOneSession(jumpSession, false)
|
||||||
|
|
||||||
this.attachSessionHandler(jumpSession.destroyed$, () => {
|
this.attachSessionHandler(jumpSession.destroyed$, () => {
|
||||||
if (session.open) {
|
if (session.open) {
|
||||||
@@ -142,7 +142,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ssh.connectSession(session)
|
await session.start(interactive)
|
||||||
this.stopSpinner()
|
this.stopSpinner()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.stopSpinner()
|
this.stopSpinner()
|
||||||
@@ -189,12 +189,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
this.setSession(session)
|
this.setSession(session)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.setupOneSession(session)
|
await this.setupOneSession(session, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.session!.start()
|
|
||||||
this.session!.resize(this.size.columns, this.size.rows)
|
this.session!.resize(this.size.columns, this.size.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ export class SSHConfigProvider extends ConfigProvider {
|
|||||||
winSCPPath: null,
|
winSCPPath: null,
|
||||||
agentType: 'auto',
|
agentType: 'auto',
|
||||||
agentPath: null,
|
agentPath: null,
|
||||||
|
x11Display: null,
|
||||||
},
|
},
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
'restart-ssh-session': [],
|
'restart-ssh-session': [],
|
||||||
|
@@ -1,156 +1,29 @@
|
|||||||
import colors from 'ansi-colors'
|
|
||||||
import * as shellQuote from 'shell-quote'
|
import * as shellQuote from 'shell-quote'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Client } from 'ssh2'
|
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { ChildProcess } from 'node:child_process'
|
import { ChildProcess } from 'node:child_process'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
||||||
import { KeyboardInteractivePrompt, SSHSession } from '../session/ssh'
|
import { SSHSession } from '../session/ssh'
|
||||||
import { ForwardedPort } from '../session/forwards'
|
import { SSHProfile } from '../api'
|
||||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
|
|
||||||
import { PasswordStorageService } from './passwordStorage.service'
|
import { PasswordStorageService } from './passwordStorage.service'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SSHService {
|
export class SSHService {
|
||||||
private logger: Logger
|
|
||||||
private detectedWinSCPPath: string | null
|
private detectedWinSCPPath: string | null
|
||||||
|
|
||||||
private constructor (
|
private constructor (
|
||||||
log: LogService,
|
|
||||||
private zone: NgZone,
|
|
||||||
private passwordStorage: PasswordStorageService,
|
private passwordStorage: PasswordStorageService,
|
||||||
private notifications: NotificationsService,
|
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
hostApp: HostAppService,
|
hostApp: HostAppService,
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('ssh')
|
|
||||||
if (hostApp.platform === Platform.Windows) {
|
if (hostApp.platform === Platform.Windows) {
|
||||||
this.detectedWinSCPPath = platform.getWinSCPPath()
|
this.detectedWinSCPPath = platform.getWinSCPPath()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectSession (session: SSHSession): Promise<void> {
|
|
||||||
const log = (s: any) => session.emitServiceMessage(s)
|
|
||||||
|
|
||||||
const ssh = new Client()
|
|
||||||
session.ssh = ssh
|
|
||||||
await session.init()
|
|
||||||
|
|
||||||
let connected = false
|
|
||||||
const algorithms = {}
|
|
||||||
for (const key of Object.values(SSHAlgorithmType)) {
|
|
||||||
algorithms[key] = session.profile.options.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
|
||||||
ssh.on('ready', () => {
|
|
||||||
connected = true
|
|
||||||
if (session.savedPassword) {
|
|
||||||
this.passwordStorage.savePassword(session.profile, session.savedPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fw of session.profile.options.forwardedPorts ?? []) {
|
|
||||||
session.addPortForward(Object.assign(new ForwardedPort(), fw))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zone.run(resolve)
|
|
||||||
})
|
|
||||||
ssh.on('handshake', negotiated => {
|
|
||||||
this.logger.info('Handshake complete:', negotiated)
|
|
||||||
})
|
|
||||||
ssh.on('error', error => {
|
|
||||||
if (error.message === 'All configured authentication methods failed') {
|
|
||||||
this.passwordStorage.deletePassword(session.profile)
|
|
||||||
}
|
|
||||||
this.zone.run(() => {
|
|
||||||
if (connected) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
||||||
this.notifications.error(error.toString())
|
|
||||||
} else {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
ssh.on('close', () => {
|
|
||||||
if (session.open) {
|
|
||||||
session.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
|
||||||
session.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
|
|
||||||
name,
|
|
||||||
instructions,
|
|
||||||
prompts.map(x => x.prompt),
|
|
||||||
finish,
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
|
|
||||||
ssh.on('greeting', greeting => {
|
|
||||||
if (!session.profile.options.skipBanner) {
|
|
||||||
log('Greeting: ' + greeting)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ssh.on('banner', banner => {
|
|
||||||
if (!session.profile.options.skipBanner) {
|
|
||||||
log(banner)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (session.profile.options.proxyCommand) {
|
|
||||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${session.profile.options.proxyCommand}`)
|
|
||||||
session.proxyCommandStream = new ProxyCommandStream(session.profile.options.proxyCommand)
|
|
||||||
|
|
||||||
session.proxyCommandStream.on('error', err => {
|
|
||||||
session.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
|
||||||
session.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
session.proxyCommandStream.output$.subscribe((message: string) => {
|
|
||||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message.trim())
|
|
||||||
})
|
|
||||||
|
|
||||||
await session.proxyCommandStream.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh.connect({
|
|
||||||
host: session.profile.options.host.trim(),
|
|
||||||
port: session.profile.options.port ?? 22,
|
|
||||||
sock: session.proxyCommandStream ?? session.jumpStream,
|
|
||||||
username: session.profile.options.user,
|
|
||||||
tryKeyboard: true,
|
|
||||||
agent: session.agentPath,
|
|
||||||
agentForward: session.profile.options.agentForward && !!session.agentPath,
|
|
||||||
keepaliveInterval: session.profile.options.keepaliveInterval ?? 15000,
|
|
||||||
keepaliveCountMax: session.profile.options.keepaliveCountMax,
|
|
||||||
readyTimeout: session.profile.options.readyTimeout,
|
|
||||||
hostVerifier: (digest: string) => {
|
|
||||||
log('Host key fingerprint:')
|
|
||||||
log(colors.white.bgBlack(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
hostHash: 'sha256' as any,
|
|
||||||
algorithms,
|
|
||||||
authHandler: (methodsLeft, partialSuccess, callback) => {
|
|
||||||
this.zone.run(async () => {
|
|
||||||
callback(await session.handleAuth(methodsLeft))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
this.notifications.error(e.message)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
getWinSCPPath (): string|undefined {
|
getWinSCPPath (): string|undefined {
|
||||||
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
||||||
}
|
}
|
||||||
|
@@ -9,15 +9,16 @@ import { Injector, NgZone } from '@angular/core'
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService } from 'tabby-core'
|
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService } from 'tabby-core'
|
||||||
import { BaseSession } from 'tabby-terminal'
|
import { BaseSession } from 'tabby-terminal'
|
||||||
import { Socket, createConnection } from 'net'
|
import { Socket } from 'net'
|
||||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { ProxyCommandStream } from '../services/ssh.service'
|
import { ProxyCommandStream } from '../services/ssh.service'
|
||||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { SFTPSession } from './sftp'
|
import { SFTPSession } from './sftp'
|
||||||
import { PortForwardType, SSHProfile } from '../api/interfaces'
|
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, PortForwardType, SSHProfile } from '../api'
|
||||||
import { ForwardedPort } from './forwards'
|
import { ForwardedPort } from './forwards'
|
||||||
|
import { X11Socket } from './x11'
|
||||||
|
|
||||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ export class SSHSession extends BaseSession {
|
|||||||
private serviceMessage = new Subject<string>()
|
private serviceMessage = new Subject<string>()
|
||||||
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
|
||||||
private keychainPasswordUsed = false
|
private keychainPasswordUsed = false
|
||||||
|
private authUsername: string|null = null
|
||||||
|
|
||||||
private passwordStorage: PasswordStorageService
|
private passwordStorage: PasswordStorageService
|
||||||
private ngbModal: NgbModal
|
private ngbModal: NgbModal
|
||||||
@@ -157,9 +159,147 @@ export class SSHSession extends BaseSession {
|
|||||||
return new SFTPSession(this.sftp, this.injector)
|
return new SFTPSession(this.sftp, this.injector)
|
||||||
}
|
}
|
||||||
|
|
||||||
async start (): Promise<void> {
|
|
||||||
|
async start (interactive = true): Promise<void> {
|
||||||
|
const log = (s: any) => this.emitServiceMessage(s)
|
||||||
|
|
||||||
|
const ssh = new Client()
|
||||||
|
this.ssh = ssh
|
||||||
|
await this.init()
|
||||||
|
|
||||||
|
let connected = false
|
||||||
|
const algorithms = {}
|
||||||
|
for (const key of Object.values(SSHAlgorithmType)) {
|
||||||
|
algorithms[key] = this.profile.options.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
||||||
|
ssh.on('ready', () => {
|
||||||
|
connected = true
|
||||||
|
if (this.savedPassword) {
|
||||||
|
this.passwordStorage.savePassword(this.profile, this.savedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fw of this.profile.options.forwardedPorts ?? []) {
|
||||||
|
this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zone.run(resolve)
|
||||||
|
})
|
||||||
|
ssh.on('handshake', negotiated => {
|
||||||
|
this.logger.info('Handshake complete:', negotiated)
|
||||||
|
})
|
||||||
|
ssh.on('error', error => {
|
||||||
|
if (error.message === 'All configured authentication methods failed') {
|
||||||
|
this.passwordStorage.deletePassword(this.profile)
|
||||||
|
}
|
||||||
|
this.zone.run(() => {
|
||||||
|
if (connected) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
this.notifications.error(error.toString())
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
ssh.on('close', () => {
|
||||||
|
if (this.open) {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||||
|
this.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
|
||||||
|
name,
|
||||||
|
instructions,
|
||||||
|
prompts.map(x => x.prompt),
|
||||||
|
finish,
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
|
||||||
|
ssh.on('greeting', greeting => {
|
||||||
|
if (!this.profile.options.skipBanner) {
|
||||||
|
log('Greeting: ' + greeting)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ssh.on('banner', banner => {
|
||||||
|
if (!this.profile.options.skipBanner) {
|
||||||
|
log(banner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.profile.options.proxyCommand) {
|
||||||
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
||||||
|
this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
||||||
|
|
||||||
|
this.proxyCommandStream.on('error', err => {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||||
|
this.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.proxyCommandStream.output$.subscribe((message: string) => {
|
||||||
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message.trim())
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.proxyCommandStream.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authUsername ??= this.profile.options.user
|
||||||
|
if (!this.authUsername) {
|
||||||
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
||||||
|
try {
|
||||||
|
const result = await modal.result
|
||||||
|
this.authUsername = result?.value ?? null
|
||||||
|
} catch {
|
||||||
|
this.authUsername = 'root'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh.connect({
|
||||||
|
host: this.profile.options.host.trim(),
|
||||||
|
port: this.profile.options.port ?? 22,
|
||||||
|
sock: this.proxyCommandStream ?? this.jumpStream,
|
||||||
|
username: this.authUsername ?? undefined,
|
||||||
|
tryKeyboard: true,
|
||||||
|
agent: this.agentPath,
|
||||||
|
agentForward: this.profile.options.agentForward && !!this.agentPath,
|
||||||
|
keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
|
||||||
|
keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
||||||
|
readyTimeout: this.profile.options.readyTimeout,
|
||||||
|
hostVerifier: (digest: string) => {
|
||||||
|
log('Host key fingerprint:')
|
||||||
|
log(colors.white.bgBlack(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
hostHash: 'sha256' as any,
|
||||||
|
algorithms,
|
||||||
|
authHandler: (methodsLeft, partialSuccess, callback) => {
|
||||||
|
this.zone.run(async () => {
|
||||||
|
const a = await this.handleAuth(methodsLeft)
|
||||||
|
console.warn(a)
|
||||||
|
callback(a)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.notifications.error(e.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
await resultPromise
|
||||||
|
|
||||||
this.open = true
|
this.open = true
|
||||||
|
|
||||||
|
if (!interactive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.shell = await this.openShellChannel({ x11: this.profile.options.x11 })
|
this.shell = await this.openShellChannel({ x11: this.profile.options.x11 })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -220,41 +360,35 @@ export class SSHSession extends BaseSession {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.ssh.on('x11', (details, accept, reject) => {
|
this.ssh.on('x11', async (details, accept, reject) => {
|
||||||
this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
|
this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
|
||||||
const displaySpec = process.env.DISPLAY ?? ':0'
|
const displaySpec = process.env.DISPLAY ?? 'localhost:0'
|
||||||
this.logger.debug(`Trying display ${displaySpec}`)
|
this.logger.debug(`Trying display ${displaySpec}`)
|
||||||
const xHost = displaySpec.split(':')[0]
|
|
||||||
const xDisplay = parseInt(displaySpec.split(':')[1].split('.')[0] || '0')
|
|
||||||
const xPort = xDisplay < 100 ? xDisplay + 6000 : xDisplay
|
|
||||||
|
|
||||||
const socket = displaySpec.startsWith('/') ? createConnection(displaySpec) : new Socket()
|
const socket = new X11Socket()
|
||||||
if (!displaySpec.startsWith('/')) {
|
try {
|
||||||
socket.connect(xPort, xHost)
|
const x11Stream = await socket.connect(displaySpec)
|
||||||
}
|
this.logger.info('Connection forwarded')
|
||||||
socket.on('error', e => {
|
const stream = accept()
|
||||||
|
stream.pipe(x11Stream)
|
||||||
|
x11Stream.pipe(stream)
|
||||||
|
stream.on('close', () => {
|
||||||
|
socket.destroy()
|
||||||
|
})
|
||||||
|
x11Stream.on('close', () => {
|
||||||
|
stream.close()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server: ${e}`)
|
||||||
this.emitServiceMessage(` Tabby tried to connect to ${xHost}:${xPort} based on the DISPLAY environment var (${displaySpec})`)
|
this.emitServiceMessage(` Tabby tried to connect to ${JSON.stringify(X11Socket.resolveDisplaySpec(displaySpec))} based on the DISPLAY environment var (${displaySpec})`)
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
|
this.emitServiceMessage(' To use X forwarding, you need a local X server, e.g.:')
|
||||||
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
|
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
|
||||||
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
|
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
|
||||||
}
|
}
|
||||||
reject()
|
reject()
|
||||||
})
|
}
|
||||||
socket.on('connect', () => {
|
|
||||||
this.logger.info('Connection forwarded')
|
|
||||||
const stream = accept()
|
|
||||||
stream.pipe(socket)
|
|
||||||
socket.pipe(stream)
|
|
||||||
stream.on('close', () => {
|
|
||||||
socket.destroy()
|
|
||||||
})
|
|
||||||
socket.on('close', () => {
|
|
||||||
stream.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,26 +426,26 @@ export class SSHSession extends BaseSession {
|
|||||||
this.emitServiceMessage('Using preset password')
|
this.emitServiceMessage('Using preset password')
|
||||||
return {
|
return {
|
||||||
type: 'password',
|
type: 'password',
|
||||||
username: this.profile.options.user,
|
username: this.authUsername,
|
||||||
password: this.profile.options.password,
|
password: this.profile.options.password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.keychainPasswordUsed) {
|
if (!this.keychainPasswordUsed && this.profile.options.user) {
|
||||||
const password = await this.passwordStorage.loadPassword(this.profile)
|
const password = await this.passwordStorage.loadPassword(this.profile)
|
||||||
if (password) {
|
if (password) {
|
||||||
this.emitServiceMessage('Trying saved password')
|
this.emitServiceMessage('Trying saved password')
|
||||||
this.keychainPasswordUsed = true
|
this.keychainPasswordUsed = true
|
||||||
return {
|
return {
|
||||||
type: 'password',
|
type: 'password',
|
||||||
username: this.profile.options.user,
|
username: this.authUsername,
|
||||||
password,
|
password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
|
modal.componentInstance.prompt = `Password for ${this.authUsername}@${this.profile.options.host}`
|
||||||
modal.componentInstance.password = true
|
modal.componentInstance.password = true
|
||||||
modal.componentInstance.showRememberCheckbox = true
|
modal.componentInstance.showRememberCheckbox = true
|
||||||
|
|
||||||
@@ -323,7 +457,7 @@ export class SSHSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: 'password',
|
type: 'password',
|
||||||
username: this.profile.options.user,
|
username: this.authUsername,
|
||||||
password: result.value,
|
password: result.value,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -338,7 +472,7 @@ export class SSHSession extends BaseSession {
|
|||||||
const key = await this.loadPrivateKey(method.contents)
|
const key = await this.loadPrivateKey(method.contents)
|
||||||
return {
|
return {
|
||||||
type: 'publickey',
|
type: 'publickey',
|
||||||
username: this.profile.options.user,
|
username: this.authUsername,
|
||||||
key,
|
key,
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
57
tabby-ssh/src/session/x11.ts
Normal file
57
tabby-ssh/src/session/x11.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Socket, SocketConnectOpts } from 'net'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
|
||||||
|
export class X11Socket {
|
||||||
|
error$ = new Subject<Error>()
|
||||||
|
private socket: Socket | null = null
|
||||||
|
|
||||||
|
static resolveDisplaySpec (spec?: string|null): SocketConnectOpts {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let [xHost, xDisplay] = /^(.+):(\d+)(?:.(\d+))$/.exec(spec ?? process.env.DISPLAY ?? 'localhost:0') ?? []
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
xHost ??= 'localhost'
|
||||||
|
} else {
|
||||||
|
xHost ??= 'unix'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec?.startsWith('/')) {
|
||||||
|
xHost = spec
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = parseInt(xDisplay || '0')
|
||||||
|
const port = display < 100 ? display + 6000 : display
|
||||||
|
|
||||||
|
if (xHost === 'unix') {
|
||||||
|
xHost = `/tmp/.X11-unix/X${display}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xHost.startsWith('/')) {
|
||||||
|
return {
|
||||||
|
path: xHost,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
host: xHost,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect (spec: string): Promise<Socket> {
|
||||||
|
this.socket = new Socket()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket!.on('connect', () => {
|
||||||
|
resolve(this.socket!)
|
||||||
|
})
|
||||||
|
this.socket!.on('error', e => {
|
||||||
|
this.error$.next(e)
|
||||||
|
reject(e)
|
||||||
|
})
|
||||||
|
this.socket!.connect(X11Socket.resolveDisplaySpec(spec))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy (): void {
|
||||||
|
this.socket?.destroy()
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@ export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScri
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum TelnetCommands {
|
enum TelnetCommands {
|
||||||
|
SUBOPTION_SEND = 1,
|
||||||
SUBOPTION_END = 240,
|
SUBOPTION_END = 240,
|
||||||
GA = 249,
|
GA = 249,
|
||||||
SUBOPTION = 250,
|
SUBOPTION = 250,
|
||||||
@@ -35,7 +36,9 @@ enum TelnetOptions {
|
|||||||
NEGO_WINDOW_SIZE = 0x1f,
|
NEGO_WINDOW_SIZE = 0x1f,
|
||||||
NEGO_TERMINAL_SPEED = 0x20,
|
NEGO_TERMINAL_SPEED = 0x20,
|
||||||
STATUS = 0x05,
|
STATUS = 0x05,
|
||||||
|
REMOTE_FLOW_CONTROL = 0x21,
|
||||||
X_DISPLAY_LOCATION = 0x23,
|
X_DISPLAY_LOCATION = 0x23,
|
||||||
|
NEW_ENVIRON = 0x27,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TelnetSession extends BaseSession {
|
export class TelnetSession extends BaseSession {
|
||||||
@@ -48,6 +51,7 @@ export class TelnetSession extends BaseSession {
|
|||||||
private echoEnabled = false
|
private echoEnabled = false
|
||||||
private lastWidth = 0
|
private lastWidth = 0
|
||||||
private lastHeight = 0
|
private lastHeight = 0
|
||||||
|
private requestedOptions = new Set<number>()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
@@ -107,6 +111,11 @@ export class TelnetSession extends BaseSession {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestOption (cmd: TelnetCommands, option: TelnetOptions): void {
|
||||||
|
this.requestedOptions.add(option)
|
||||||
|
this.emitTelnet(cmd, option)
|
||||||
|
}
|
||||||
|
|
||||||
emitServiceMessage (msg: string): void {
|
emitServiceMessage (msg: string): void {
|
||||||
this.serviceMessage.next(msg)
|
this.serviceMessage.next(msg)
|
||||||
this.logger.info(stripAnsi(msg))
|
this.logger.info(stripAnsi(msg))
|
||||||
@@ -115,7 +124,7 @@ export class TelnetSession extends BaseSession {
|
|||||||
onData (data: Buffer): void {
|
onData (data: Buffer): void {
|
||||||
if (!this.telnetProtocol && data[0] === TelnetCommands.IAC) {
|
if (!this.telnetProtocol && data[0] === TelnetCommands.IAC) {
|
||||||
this.telnetProtocol = true
|
this.telnetProtocol = true
|
||||||
this.emitTelnet(TelnetCommands.DO, TelnetOptions.SUPPRESS_GO_AHEAD)
|
this.requestOption(TelnetCommands.DO, TelnetOptions.SUPPRESS_GO_AHEAD)
|
||||||
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.TERMINAL_TYPE)
|
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.TERMINAL_TYPE)
|
||||||
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.NEGO_WINDOW_SIZE)
|
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.NEGO_WINDOW_SIZE)
|
||||||
}
|
}
|
||||||
@@ -126,7 +135,7 @@ export class TelnetSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emitTelnet (command: TelnetCommands, option: TelnetOptions): void {
|
emitTelnet (command: TelnetCommands, option: TelnetOptions): void {
|
||||||
this.logger.debug('>', TelnetCommands[command], TelnetOptions[option])
|
this.logger.debug('>', TelnetCommands[command], TelnetOptions[option] || option)
|
||||||
this.socket.write(Buffer.from([TelnetCommands.IAC, command, option]))
|
this.socket.write(Buffer.from([TelnetCommands.IAC, command, option]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +166,14 @@ export class TelnetSession extends BaseSession {
|
|||||||
|
|
||||||
data = data.slice(3)
|
data = data.slice(3)
|
||||||
this.logger.debug('<', commandName || command, optionName || option)
|
this.logger.debug('<', commandName || command, optionName || option)
|
||||||
|
|
||||||
|
if (command === TelnetCommands.WILL || command === TelnetCommands.WONT) {
|
||||||
|
if (this.requestedOptions.has(option)) {
|
||||||
|
this.requestedOptions.delete(option)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (command === TelnetCommands.WILL) {
|
if (command === TelnetCommands.WILL) {
|
||||||
if ([
|
if ([
|
||||||
TelnetOptions.SUPPRESS_GO_AHEAD,
|
TelnetOptions.SUPPRESS_GO_AHEAD,
|
||||||
@@ -170,14 +187,13 @@ export class TelnetSession extends BaseSession {
|
|||||||
}
|
}
|
||||||
if (command === TelnetCommands.DO) {
|
if (command === TelnetCommands.DO) {
|
||||||
if (option === TelnetOptions.NEGO_WINDOW_SIZE) {
|
if (option === TelnetOptions.NEGO_WINDOW_SIZE) {
|
||||||
this.emitSize()
|
|
||||||
this.emitTelnet(TelnetCommands.WILL, option)
|
this.emitTelnet(TelnetCommands.WILL, option)
|
||||||
|
this.emitSize()
|
||||||
} else if (option === TelnetOptions.ECHO) {
|
} else if (option === TelnetOptions.ECHO) {
|
||||||
this.echoEnabled = true
|
this.echoEnabled = true
|
||||||
this.emitTelnet(TelnetCommands.WILL, option)
|
this.emitTelnet(TelnetCommands.WILL, option)
|
||||||
} else if (option === TelnetOptions.TERMINAL_TYPE) {
|
} else if (option === TelnetOptions.TERMINAL_TYPE) {
|
||||||
this.emitTelnet(TelnetCommands.WILL, option)
|
this.emitTelnet(TelnetCommands.WILL, option)
|
||||||
this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')]))
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('(!) Unhandled option')
|
this.logger.debug('(!) Unhandled option')
|
||||||
this.emitTelnet(TelnetCommands.WONT, option)
|
this.emitTelnet(TelnetCommands.WONT, option)
|
||||||
@@ -196,6 +212,11 @@ export class TelnetSession extends BaseSession {
|
|||||||
const endIndex = data.indexOf(TelnetCommands.IAC)
|
const endIndex = data.indexOf(TelnetCommands.IAC)
|
||||||
const optionValue = data.slice(0, endIndex)
|
const optionValue = data.slice(0, endIndex)
|
||||||
this.logger.debug('<', commandName || command, optionName || option, optionValue)
|
this.logger.debug('<', commandName || command, optionName || option, optionValue)
|
||||||
|
|
||||||
|
if (option === TelnetOptions.TERMINAL_TYPE && optionValue[0] === TelnetCommands.SUBOPTION_SEND) {
|
||||||
|
this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')]))
|
||||||
|
}
|
||||||
|
|
||||||
data = data.slice(endIndex + 2)
|
data = data.slice(endIndex + 2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -28,7 +28,6 @@
|
|||||||
"hexer": "^1.5.0",
|
"hexer": "^1.5.0",
|
||||||
"ps-node": "^0.1.6",
|
"ps-node": "^0.1.6",
|
||||||
"runes": "^0.4.2",
|
"runes": "^0.4.2",
|
||||||
"utils-decorators": "^1.8.1",
|
|
||||||
"xterm": "npm:@tabby-gang/xterm@4.14.0",
|
"xterm": "npm:@tabby-gang/xterm@4.14.0",
|
||||||
"xterm-addon-fit": "^0.5.0",
|
"xterm-addon-fit": "^0.5.0",
|
||||||
"xterm-addon-ligatures": "^0.5.0",
|
"xterm-addon-ligatures": "^0.5.0",
|
||||||
|
@@ -201,15 +201,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.frontend.clearSelection()
|
this.frontend.clearSelection()
|
||||||
this.notifications.notice('Copied')
|
this.notifications.notice('Copied')
|
||||||
} else {
|
} else {
|
||||||
if (this.parent && this.parent instanceof SplitTabComponent && this.parent._allFocusMode) {
|
this.forEachFocusedTerminalPane(tab => tab.sendInput('\x03'))
|
||||||
for (const tab of this.parent.getAllTabs()) {
|
|
||||||
if (tab instanceof BaseTerminalTabComponent) {
|
|
||||||
tab.sendInput('\x03')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.sendInput('\x03')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'copy':
|
case 'copy':
|
||||||
@@ -218,46 +210,54 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.notifications.notice('Copied')
|
this.notifications.notice('Copied')
|
||||||
break
|
break
|
||||||
case 'paste':
|
case 'paste':
|
||||||
this.paste()
|
this.forEachFocusedTerminalPane(tab => tab.paste())
|
||||||
break
|
break
|
||||||
case 'select-all':
|
case 'select-all':
|
||||||
this.frontend?.selectAll()
|
this.frontend?.selectAll()
|
||||||
break
|
break
|
||||||
case 'clear':
|
case 'clear':
|
||||||
this.frontend?.clear()
|
this.forEachFocusedTerminalPane(tab => tab.frontend?.clear())
|
||||||
break
|
break
|
||||||
case 'zoom-in':
|
case 'zoom-in':
|
||||||
this.zoomIn()
|
this.forEachFocusedTerminalPane(tab => tab.zoomIn())
|
||||||
break
|
break
|
||||||
case 'zoom-out':
|
case 'zoom-out':
|
||||||
this.zoomOut()
|
this.forEachFocusedTerminalPane(tab => tab.zoomOut())
|
||||||
break
|
break
|
||||||
case 'reset-zoom':
|
case 'reset-zoom':
|
||||||
this.resetZoom()
|
this.forEachFocusedTerminalPane(tab => tab.resetZoom())
|
||||||
break
|
break
|
||||||
case 'previous-word':
|
case 'previous-word':
|
||||||
this.sendInput({
|
this.forEachFocusedTerminalPane(tab => {
|
||||||
[Platform.Windows]: '\x1b[1;5D',
|
tab.sendInput({
|
||||||
[Platform.macOS]: '\x1bb',
|
[Platform.Windows]: '\x1b[1;5D',
|
||||||
[Platform.Linux]: '\x1bb',
|
[Platform.macOS]: '\x1bb',
|
||||||
}[this.hostApp.platform])
|
[Platform.Linux]: '\x1bb',
|
||||||
|
}[this.hostApp.platform])
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'next-word':
|
case 'next-word':
|
||||||
this.sendInput({
|
this.forEachFocusedTerminalPane(tab => {
|
||||||
[Platform.Windows]: '\x1b[1;5C',
|
tab.sendInput({
|
||||||
[Platform.macOS]: '\x1bf',
|
[Platform.Windows]: '\x1b[1;5C',
|
||||||
[Platform.Linux]: '\x1bf',
|
[Platform.macOS]: '\x1bf',
|
||||||
}[this.hostApp.platform])
|
[Platform.Linux]: '\x1bf',
|
||||||
|
}[this.hostApp.platform])
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'delete-previous-word':
|
case 'delete-previous-word':
|
||||||
this.sendInput('\x1b\x7f')
|
this.forEachFocusedTerminalPane(tab => {
|
||||||
|
tab.sendInput('\x1b\x7f')
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'delete-next-word':
|
case 'delete-next-word':
|
||||||
this.sendInput({
|
this.forEachFocusedTerminalPane(tab => {
|
||||||
[Platform.Windows]: '\x1bd\x1b[3;5~',
|
tab.sendInput({
|
||||||
[Platform.macOS]: '\x1bd',
|
[Platform.Windows]: '\x1bd\x1b[3;5~',
|
||||||
[Platform.Linux]: '\x1bd',
|
[Platform.macOS]: '\x1bd',
|
||||||
}[this.hostApp.platform])
|
[Platform.Linux]: '\x1bd',
|
||||||
|
}[this.hostApp.platform])
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'search':
|
case 'search':
|
||||||
this.showSearchPanel = true
|
this.showSearchPanel = true
|
||||||
@@ -422,15 +422,16 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
|
|
||||||
async paste (): Promise<void> {
|
async paste (): Promise<void> {
|
||||||
let data = this.platform.readClipboard()
|
let data = this.platform.readClipboard()
|
||||||
if (this.config.store.terminal.bracketedPaste && this.frontend?.supportsBracketedPaste()) {
|
|
||||||
data = `\x1b[200~${data}\x1b[201~`
|
|
||||||
}
|
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
data = data.replaceAll('\r\n', '\r')
|
data = data.replaceAll('\r\n', '\r')
|
||||||
} else {
|
} else {
|
||||||
data = data.replaceAll('\n', '\r')
|
data = data.replaceAll('\n', '\r')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.endsWith('\n')) {
|
||||||
|
data = data.substring(0, data.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.alternateScreenActive) {
|
if (!this.alternateScreenActive) {
|
||||||
data = data.trim()
|
data = data.trim()
|
||||||
|
|
||||||
@@ -451,6 +452,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.store.terminal.bracketedPaste && this.frontend?.supportsBracketedPaste()) {
|
||||||
|
data = `\x1b[200~${data}\x1b[201~`
|
||||||
|
}
|
||||||
this.sendInput(data)
|
this.sendInput(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +706,11 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.attachSessionHandler(this.session.destroyed$, () => {
|
this.attachSessionHandler(this.session.destroyed$, () => {
|
||||||
this.setSession(null)
|
this.setSession(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.attachSessionHandler(this.session.oscProcessor.copyRequested$, content => {
|
||||||
|
this.platform.setClipboard({ text: content })
|
||||||
|
this.notifications.notice('Copied')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
protected detachSessionHandlers (): void {
|
protected detachSessionHandlers (): void {
|
||||||
@@ -737,4 +747,16 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
|
|||||||
this.startSpinner()
|
this.startSpinner()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected forEachFocusedTerminalPane (cb: (tab: BaseTerminalTabComponent) => void): void {
|
||||||
|
if (this.parent && this.parent instanceof SplitTabComponent && this.parent._allFocusMode) {
|
||||||
|
for (const tab of this.parent.getAllTabs()) {
|
||||||
|
if (tab instanceof BaseTerminalTabComponent) {
|
||||||
|
cb(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,32 +1,46 @@
|
|||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
|
|
||||||
const OSC1337Prefix = Buffer.from('\x1b]1337;')
|
const OSCPrefix = Buffer.from('\x1b]')
|
||||||
const OSC1337Suffix = Buffer.from('\x07')
|
const OSCSuffix = Buffer.from('\x07')
|
||||||
|
|
||||||
export class OSC1337Processor {
|
export class OSCProcessor {
|
||||||
get cwdReported$ (): Observable<string> { return this.cwdReported }
|
get cwdReported$ (): Observable<string> { return this.cwdReported }
|
||||||
|
get copyRequested$ (): Observable<string> { return this.copyRequested }
|
||||||
|
|
||||||
private cwdReported = new Subject<string>()
|
private cwdReported = new Subject<string>()
|
||||||
|
private copyRequested = new Subject<string>()
|
||||||
|
|
||||||
process (data: Buffer): Buffer {
|
process (data: Buffer): Buffer {
|
||||||
if (data.includes(OSC1337Prefix)) {
|
let startIndex = 0
|
||||||
const preData = data.subarray(0, data.indexOf(OSC1337Prefix))
|
while (data.includes(OSCPrefix, startIndex) && data.includes(OSCSuffix, startIndex)) {
|
||||||
const params = data.subarray(data.indexOf(OSC1337Prefix) + OSC1337Prefix.length)
|
const params = data.subarray(data.indexOf(OSCPrefix, startIndex) + OSCPrefix.length)
|
||||||
const postData = params.subarray(params.indexOf(OSC1337Suffix) + OSC1337Suffix.length)
|
const oscString = params.subarray(0, params.indexOf(OSCSuffix)).toString()
|
||||||
const paramString = params.subarray(0, params.indexOf(OSC1337Suffix)).toString()
|
|
||||||
|
|
||||||
if (paramString.startsWith('CurrentDir=')) {
|
startIndex = data.indexOf(OSCSuffix, startIndex) + OSCSuffix.length
|
||||||
let reportedCWD = paramString.split('=')[1]
|
|
||||||
if (reportedCWD.startsWith('~')) {
|
const [oscCodeString, ...oscParams] = oscString.split(';')
|
||||||
reportedCWD = os.homedir() + reportedCWD.substring(1)
|
const oscCode = parseInt(oscCodeString)
|
||||||
|
|
||||||
|
if (oscCode === 1337) {
|
||||||
|
const paramString = oscParams.join(';')
|
||||||
|
if (paramString.startsWith('CurrentDir=')) {
|
||||||
|
let reportedCWD = paramString.split('=')[1]
|
||||||
|
if (reportedCWD.startsWith('~')) {
|
||||||
|
reportedCWD = os.homedir() + reportedCWD.substring(1)
|
||||||
|
}
|
||||||
|
this.cwdReported.next(reportedCWD)
|
||||||
|
} else {
|
||||||
|
console.debug('Unsupported OSC 1337 parameter:', paramString)
|
||||||
|
}
|
||||||
|
} else if (oscCode === 52) {
|
||||||
|
if (oscParams[0] === 'c') {
|
||||||
|
const content = Buffer.from(oscParams[1], 'base64')
|
||||||
|
this.copyRequested.next(content.toString())
|
||||||
}
|
}
|
||||||
this.cwdReported.next(reportedCWD)
|
|
||||||
} else {
|
} else {
|
||||||
console.debug('Unsupported OSC 1337 parameter:', paramString)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
data = Buffer.concat([preData, postData])
|
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
@@ -33,88 +33,89 @@ h3.mb-3 Appearance
|
|||||||
.col-12.col-md-6
|
.col-12.col-md-6
|
||||||
color-scheme-preview([scheme]='config.store.terminal.colorScheme', [fontPreview]='true')
|
color-scheme-preview([scheme]='config.store.terminal.colorScheme', [fontPreview]='true')
|
||||||
|
|
||||||
.form-line
|
.content-box
|
||||||
.header
|
.form-line
|
||||||
.title Terminal background
|
.header
|
||||||
|
.title Terminal background
|
||||||
|
|
||||||
.btn-group(
|
.btn-group(
|
||||||
[(ngModel)]='config.store.terminal.background',
|
[(ngModel)]='config.store.terminal.background',
|
||||||
(ngModelChange)='config.save()',
|
(ngModelChange)='config.save()',
|
||||||
ngbRadioGroup
|
ngbRadioGroup
|
||||||
|
)
|
||||||
|
label.btn.btn-secondary(ngbButtonLabel)
|
||||||
|
input(
|
||||||
|
type='radio',
|
||||||
|
ngbButton,
|
||||||
|
[value]='"theme"'
|
||||||
|
)
|
||||||
|
| From theme
|
||||||
|
label.btn.btn-secondary(ngbButtonLabel)
|
||||||
|
input(
|
||||||
|
type='radio',
|
||||||
|
ngbButton,
|
||||||
|
[value]='"colorScheme"'
|
||||||
|
)
|
||||||
|
| From color scheme
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Cursor shape
|
||||||
|
|
||||||
|
.btn-group(
|
||||||
|
[(ngModel)]='config.store.terminal.cursor',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
ngbRadioGroup
|
||||||
|
)
|
||||||
|
label.btn.btn-secondary(ngbButtonLabel)
|
||||||
|
input(
|
||||||
|
type='radio',
|
||||||
|
ngbButton,
|
||||||
|
[value]='"block"'
|
||||||
|
)
|
||||||
|
| █
|
||||||
|
label.btn.btn-secondary(ngbButtonLabel)
|
||||||
|
input(
|
||||||
|
type='radio',
|
||||||
|
ngbButton,
|
||||||
|
[value]='"beam"'
|
||||||
|
)
|
||||||
|
| |
|
||||||
|
label.btn.btn-secondary(ngbButtonLabel)
|
||||||
|
input(
|
||||||
|
type='radio',
|
||||||
|
ngbButton,
|
||||||
|
[value]='"underline"'
|
||||||
|
)
|
||||||
|
| ▁
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Blink cursor
|
||||||
|
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.terminal.cursorBlink',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Fallback font
|
||||||
|
.description A second font family used to display characters missing in the main font
|
||||||
|
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[ngbTypeahead]='fontAutocomplete',
|
||||||
|
[(ngModel)]='config.store.terminal.fallbackFont',
|
||||||
|
(ngModelChange)='config.save()'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Custom CSS
|
||||||
|
|
||||||
|
textarea.form-control.mb-5(
|
||||||
|
[(ngModel)]='config.store.appearance.css',
|
||||||
|
(ngModelChange)='saveConfiguration()',
|
||||||
)
|
)
|
||||||
label.btn.btn-secondary(ngbButtonLabel)
|
|
||||||
input(
|
|
||||||
type='radio',
|
|
||||||
ngbButton,
|
|
||||||
[value]='"theme"'
|
|
||||||
)
|
|
||||||
| From theme
|
|
||||||
label.btn.btn-secondary(ngbButtonLabel)
|
|
||||||
input(
|
|
||||||
type='radio',
|
|
||||||
ngbButton,
|
|
||||||
[value]='"colorScheme"'
|
|
||||||
)
|
|
||||||
| From color scheme
|
|
||||||
|
|
||||||
.form-line
|
|
||||||
.header
|
|
||||||
.title Cursor shape
|
|
||||||
|
|
||||||
.btn-group(
|
|
||||||
[(ngModel)]='config.store.terminal.cursor',
|
|
||||||
(ngModelChange)='config.save()',
|
|
||||||
ngbRadioGroup
|
|
||||||
)
|
|
||||||
label.btn.btn-secondary(ngbButtonLabel)
|
|
||||||
input(
|
|
||||||
type='radio',
|
|
||||||
ngbButton,
|
|
||||||
[value]='"block"'
|
|
||||||
)
|
|
||||||
| █
|
|
||||||
label.btn.btn-secondary(ngbButtonLabel)
|
|
||||||
input(
|
|
||||||
type='radio',
|
|
||||||
ngbButton,
|
|
||||||
[value]='"beam"'
|
|
||||||
)
|
|
||||||
| |
|
|
||||||
label.btn.btn-secondary(ngbButtonLabel)
|
|
||||||
input(
|
|
||||||
type='radio',
|
|
||||||
ngbButton,
|
|
||||||
[value]='"underline"'
|
|
||||||
)
|
|
||||||
| ▁
|
|
||||||
|
|
||||||
.form-line
|
|
||||||
.header
|
|
||||||
.title Blink cursor
|
|
||||||
|
|
||||||
toggle(
|
|
||||||
[(ngModel)]='config.store.terminal.cursorBlink',
|
|
||||||
(ngModelChange)='config.save()',
|
|
||||||
)
|
|
||||||
|
|
||||||
.form-line
|
|
||||||
.header
|
|
||||||
.title Fallback font
|
|
||||||
.description A second font family used to display characters missing in the main font
|
|
||||||
|
|
||||||
input.form-control(
|
|
||||||
type='text',
|
|
||||||
[ngbTypeahead]='fontAutocomplete',
|
|
||||||
[(ngModel)]='config.store.terminal.fallbackFont',
|
|
||||||
(ngModelChange)='config.save()'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
.form-line
|
|
||||||
.header
|
|
||||||
.title Custom CSS
|
|
||||||
|
|
||||||
textarea.form-control.mb-5(
|
|
||||||
[(ngModel)]='config.store.appearance.css',
|
|
||||||
(ngModelChange)='saveConfiguration()',
|
|
||||||
)
|
|
||||||
|
@@ -12,4 +12,5 @@ div(
|
|||||||
autoClose='outside',
|
autoClose='outside',
|
||||||
container='body',
|
container='body',
|
||||||
#popover='ngbPopover',
|
#popover='ngbPopover',
|
||||||
|
[title]='hint'
|
||||||
) {{ title }}
|
) {{ title }}
|
||||||
|
@@ -10,6 +10,7 @@ import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
export class ColorPickerComponent {
|
export class ColorPickerComponent {
|
||||||
@Input() model: string
|
@Input() model: string
|
||||||
@Input() title: string
|
@Input() title: string
|
||||||
|
@Input() hint: string
|
||||||
@Output() modelChange = new EventEmitter<string>()
|
@Output() modelChange = new EventEmitter<string>()
|
||||||
@ViewChild('popover') popover: NgbPopover
|
@ViewChild('popover') popover: NgbPopover
|
||||||
|
|
||||||
|
@@ -26,22 +26,32 @@
|
|||||||
[(model)]='config.store.terminal.colorScheme.foreground',
|
[(model)]='config.store.terminal.colorScheme.foreground',
|
||||||
(modelChange)='config.save()',
|
(modelChange)='config.save()',
|
||||||
title='FG',
|
title='FG',
|
||||||
|
hint='Foreground'
|
||||||
)
|
)
|
||||||
color-picker(
|
color-picker(
|
||||||
[(model)]='config.store.terminal.colorScheme.background',
|
[(model)]='config.store.terminal.colorScheme.background',
|
||||||
(modelChange)='config.save()',
|
(modelChange)='config.save()',
|
||||||
title='BG',
|
title='BG',
|
||||||
|
hint='Background'
|
||||||
)
|
)
|
||||||
color-picker(
|
color-picker(
|
||||||
[(model)]='config.store.terminal.colorScheme.cursor',
|
[(model)]='config.store.terminal.colorScheme.cursor',
|
||||||
(modelChange)='config.save()',
|
(modelChange)='config.save()',
|
||||||
title='CU',
|
title='CU',
|
||||||
|
hint='Cursor color'
|
||||||
|
)
|
||||||
|
color-picker(
|
||||||
|
[(model)]='config.store.terminal.colorScheme.cursorAccent',
|
||||||
|
(modelChange)='config.save()',
|
||||||
|
title='CA',
|
||||||
|
hint='Block cursor foreground'
|
||||||
)
|
)
|
||||||
color-picker(
|
color-picker(
|
||||||
*ngFor='let _ of config.store.terminal.colorScheme.colors; let idx = index; trackBy: colorsTrackBy',
|
*ngFor='let _ of config.store.terminal.colorScheme.colors; let idx = index; trackBy: colorsTrackBy',
|
||||||
[(model)]='config.store.terminal.colorScheme.colors[idx]',
|
[(model)]='config.store.terminal.colorScheme.colors[idx]',
|
||||||
(modelChange)='config.save()',
|
(modelChange)='config.save()',
|
||||||
[title]='idx',
|
[title]='idx',
|
||||||
|
hint='ANSI color {{idx}}'
|
||||||
)
|
)
|
||||||
|
|
||||||
color-scheme-preview([scheme]='config.store.terminal.colorScheme')
|
color-scheme-preview([scheme]='config.store.terminal.colorScheme')
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import deepEqual from 'deep-equal'
|
import deepEqual from 'deep-equal'
|
||||||
|
|
||||||
import { Component, Inject, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'
|
import { Component, Inject, Input, ChangeDetectionStrategy, ChangeDetectorRef, HostBinding } from '@angular/core'
|
||||||
import { ConfigService, PlatformService } from 'tabby-core'
|
import { ConfigService, PlatformService } from 'tabby-core'
|
||||||
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
|
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
|
||||||
import { TerminalColorScheme } from '../api/interfaces'
|
import { TerminalColorScheme } from '../api/interfaces'
|
||||||
@@ -23,6 +23,8 @@ export class ColorSchemeSettingsTabComponent {
|
|||||||
currentStockScheme: TerminalColorScheme|null = null
|
currentStockScheme: TerminalColorScheme|null = null
|
||||||
currentCustomScheme: TerminalColorScheme|null = null
|
currentCustomScheme: TerminalColorScheme|null = null
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
||||||
private changeDetector: ChangeDetectorRef,
|
private changeDetector: ChangeDetectorRef,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -8,6 +8,8 @@ import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-
|
|||||||
export class TerminalSettingsTabComponent {
|
export class TerminalSettingsTabComponent {
|
||||||
Platform = Platform
|
Platform = Platform
|
||||||
|
|
||||||
|
@HostBinding('class.content-box') true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
|
@@ -32,6 +32,7 @@ export class TerminalConfigProvider extends ConfigProvider {
|
|||||||
background: 'rgba(38, 50, 56, 1)',
|
background: 'rgba(38, 50, 56, 1)',
|
||||||
selection: null,
|
selection: null,
|
||||||
cursor: '#FFCC00',
|
cursor: '#FFCC00',
|
||||||
|
cursorAccent: null,
|
||||||
colors: [
|
colors: [
|
||||||
'#000000',
|
'#000000',
|
||||||
'#D62341',
|
'#D62341',
|
||||||
|
@@ -206,6 +206,9 @@ export class XTermFrontend extends Frontend {
|
|||||||
|
|
||||||
copySelection (): void {
|
copySelection (): void {
|
||||||
const text = this.getSelection()
|
const text = this.getSelection()
|
||||||
|
if (!text.trim().length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (text.length < 1024 * 32) {
|
if (text.length < 1024 * 32) {
|
||||||
this.platformService.setClipboard({
|
this.platformService.setClipboard({
|
||||||
text: this.getSelection(),
|
text: this.getSelection(),
|
||||||
@@ -286,6 +289,7 @@ export class XTermFrontend extends Frontend {
|
|||||||
selection: config.terminal.colorScheme.selection || '#88888888',
|
selection: config.terminal.colorScheme.selection || '#88888888',
|
||||||
background: config.terminal.background === 'colorScheme' ? config.terminal.colorScheme.background : '#00000000',
|
background: config.terminal.background === 'colorScheme' ? config.terminal.colorScheme.background : '#00000000',
|
||||||
cursor: config.terminal.colorScheme.cursor,
|
cursor: config.terminal.colorScheme.cursor,
|
||||||
|
cursorAccent: config.terminal.colorScheme.cursorAccent,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < COLOR_NAMES.length; i++) {
|
for (let i = 0; i < COLOR_NAMES.length; i++) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { Logger } from 'tabby-core'
|
import { Logger } from 'tabby-core'
|
||||||
import { LoginScriptProcessor, LoginScriptsOptions } from './api/loginScriptProcessing'
|
import { LoginScriptProcessor, LoginScriptsOptions } from './api/loginScriptProcessing'
|
||||||
import { OSC1337Processor } from './api/osc1337Processing'
|
import { OSCProcessor } from './api/osc1337Processing'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A session object for a [[BaseTerminalTabComponent]]
|
* A session object for a [[BaseTerminalTabComponent]]
|
||||||
@@ -10,13 +10,13 @@ import { OSC1337Processor } from './api/osc1337Processing'
|
|||||||
export abstract class BaseSession {
|
export abstract class BaseSession {
|
||||||
open: boolean
|
open: boolean
|
||||||
truePID?: number
|
truePID?: number
|
||||||
|
oscProcessor = new OSCProcessor()
|
||||||
protected output = new Subject<string>()
|
protected output = new Subject<string>()
|
||||||
protected binaryOutput = new Subject<Buffer>()
|
protected binaryOutput = new Subject<Buffer>()
|
||||||
protected closed = new Subject<void>()
|
protected closed = new Subject<void>()
|
||||||
protected destroyed = new Subject<void>()
|
protected destroyed = new Subject<void>()
|
||||||
protected loginScriptProcessor: LoginScriptProcessor | null = null
|
protected loginScriptProcessor: LoginScriptProcessor | null = null
|
||||||
protected reportedCWD?: string
|
protected reportedCWD?: string
|
||||||
protected osc1337Processor = new OSC1337Processor()
|
|
||||||
private initialDataBuffer = Buffer.from('')
|
private initialDataBuffer = Buffer.from('')
|
||||||
private initialDataBufferReleased = false
|
private initialDataBufferReleased = false
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ export abstract class BaseSession {
|
|||||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||||
|
|
||||||
constructor (protected logger: Logger) {
|
constructor (protected logger: Logger) {
|
||||||
this.osc1337Processor.cwdReported$.subscribe(cwd => {
|
this.oscProcessor.cwdReported$.subscribe(cwd => {
|
||||||
this.reportedCWD = cwd
|
this.reportedCWD = cwd
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emitOutput (data: Buffer): void {
|
emitOutput (data: Buffer): void {
|
||||||
data = this.osc1337Processor.process(data)
|
data = this.oscProcessor.process(data)
|
||||||
if (!this.initialDataBufferReleased) {
|
if (!this.initialDataBufferReleased) {
|
||||||
this.initialDataBuffer = Buffer.concat([this.initialDataBuffer, data])
|
this.initialDataBuffer = Buffer.concat([this.initialDataBuffer, data])
|
||||||
} else {
|
} else {
|
||||||
@@ -64,7 +64,7 @@ export abstract class BaseSession {
|
|||||||
this.destroyed.next()
|
this.destroyed.next()
|
||||||
await this.gracefullyKillProcess()
|
await this.gracefullyKillProcess()
|
||||||
}
|
}
|
||||||
this.osc1337Processor.close()
|
this.oscProcessor.close()
|
||||||
this.closed.complete()
|
this.closed.complete()
|
||||||
this.destroyed.complete()
|
this.destroyed.complete()
|
||||||
this.output.complete()
|
this.output.complete()
|
||||||
|
@@ -11,6 +11,7 @@ export class AppearanceSettingsTabProvider extends SettingsTabProvider {
|
|||||||
id = 'terminal-appearance'
|
id = 'terminal-appearance'
|
||||||
icon = 'swatchbook'
|
icon = 'swatchbook'
|
||||||
title = 'Appearance'
|
title = 'Appearance'
|
||||||
|
prioritized = true
|
||||||
|
|
||||||
getComponentType (): any {
|
getComponentType (): any {
|
||||||
return AppearanceSettingsTabComponent
|
return AppearanceSettingsTabComponent
|
||||||
@@ -35,6 +36,7 @@ export class TerminalSettingsTabProvider extends SettingsTabProvider {
|
|||||||
id = 'terminal'
|
id = 'terminal'
|
||||||
icon = 'terminal'
|
icon = 'terminal'
|
||||||
title = 'Terminal'
|
title = 'Terminal'
|
||||||
|
prioritized = true
|
||||||
|
|
||||||
getComponentType (): any {
|
getComponentType (): any {
|
||||||
return TerminalSettingsTabComponent
|
return TerminalSettingsTabComponent
|
||||||
|
@@ -153,18 +153,6 @@ tiny-inflate@^1.0.2, tiny-inflate@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
||||||
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
||||||
|
|
||||||
tinyqueue@^2.0.3:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
|
|
||||||
integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
|
|
||||||
|
|
||||||
utils-decorators@^1.8.1:
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/utils-decorators/-/utils-decorators-1.10.0.tgz#eb9208ccbb7fbb7488d5d04b2611df62c2fcaf4d"
|
|
||||||
integrity sha512-wlNRoPCFdxSReLfmhNqkZsg8FqsKu9d5trdSELxBZCtmK4KPtSidxRg24+bpZQjEBBF0hUIQEFz2uM7sBDVG2Q==
|
|
||||||
dependencies:
|
|
||||||
tinyqueue "^2.0.3"
|
|
||||||
|
|
||||||
xtend@^4.0.0:
|
xtend@^4.0.0:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
Binary file not shown.
@@ -4,7 +4,7 @@ import { ConfigProvider } from 'tabby-core'
|
|||||||
export class WebConfigProvider extends ConfigProvider {
|
export class WebConfigProvider extends ConfigProvider {
|
||||||
defaults = {
|
defaults = {
|
||||||
web: {
|
web: {
|
||||||
preventAccidentalTabClosure: true,
|
preventAccidentalTabClosure: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,12 +12,15 @@ export class WebHostWindow extends HostWindowService {
|
|||||||
this.windowShown.next()
|
this.windowShown.next()
|
||||||
this.windowFocused.next()
|
this.windowFocused.next()
|
||||||
|
|
||||||
window.addEventListener('beforeunload', (event) => {
|
const unloadHandler = (event) => {
|
||||||
if (config.store.web.preventAccidentalTabClosure) {
|
if (config.store.web.preventAccidentalTabClosure) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.returnValue = 'Are you sure you want to close Tabby? You can disable this prompt in Settings -> Window.'
|
event.returnValue = 'Are you sure you want to close Tabby? You can disable this prompt in Settings -> Window.'
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('beforeunload', unloadHandler)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
window.addEventListener('beforeunload', unloadHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
reload (): void {
|
reload (): void {
|
||||||
|
Reference in New Issue
Block a user